一、先看一段输出题
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:1 4 3 2
原因:同步代码先执行完(1、4),然后清空微任务队列(Promise.then → 3),最后才轮到宏任务(setTimeout → 2)。setTimeout(fn, 0) 不是「立刻」,而是「下一轮宏任务」。
await 的本质也是微任务:await 后面的代码会被放进微任务队列,等当前同步代码跑完再继续。
二、Promise 的三个状态
pending:进行中。fulfilled:成功,then的第一个回调接收值。rejected:失败,then的第二个回调或catch接收原因。
状态一旦改变就不可逆,且只能改一次。这带来一个重要特性:Promise 可以被多次 then,但结果只算一次。
const p = fetch('/api/user');
p.then(r => r.json()).then(console.log);
p.catch(console.error); // 同一个 p,可以挂多个处理
三、async/await 只是语法糖
async function load() {
const res = await fetch('/api/user'); // 等待 Promise 落定
return res.json(); // 返回值会被包成 Promise
}
要点:
async函数总是返回 Promise,return 1等于Promise.resolve(1)。await只能用在async函数里(顶层 await 在 ES Module 中可用)。await一个非 Promise 值会直接返回该值,不会报错。
四、四个高频错误
1. 在循环里串行 await
// 慢:10 个请求依次等,总耗时 = 累加
for (const id of ids) { await fetch('/api/' + id); }
// 快:并发发出,总耗时 ≈ 最慢的那个
await Promise.all(ids.map(id => fetch('/api/' + id)));
如果必须限流(比如最多 5 个并发),要自己写池子,而不是改回串行。
2. 忘了 catch
await 抛出的异常会中断当前函数,如果调用方也没包 try/catch,就变成 unhandled rejection。建议:
try {
const data = await load();
} catch (e) {
console.error('加载失败', e);
}
3. Promise.all 的「一票否决」
Promise.all 只要有一个 reject 就整体 reject,其它请求仍在跑但结果被丢弃。需要「全部结果都要,不管成功失败」时用 Promise.allSettled:
const results = await Promise.allSettled(tasks);
const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value);
4. forEach 里的 await 不生效
arr.forEach(async item => { await save(item); }); // 外层不会等待
forEach 不关心回调返回值。要顺序执行用 for...of,要并发用 map + Promise.all。
五、并发控制的小工具
async function pool(items, limit, worker) {
const ret = [];
let i = 0;
const runners = Array.from({ length: limit }, async () => {
while (i < items.length) {
const idx = i++;
ret[idx] = await worker(items[idx]);
}
});
await Promise.all(runners);
return ret;
}
这是爬取、批量上传场景的常用模式:控制并发数,避免把对方打挂或本地连接耗尽。
六、超时与取消
fetch 本身没有超时,需要 AbortController:
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 8000);
try {
const res = await fetch(url, { signal: ctrl.signal });
} finally {
clearTimeout(t);
}
注意 finally 里清定时器,否则请求成功后定时器仍会在 8 秒后触发 abort(虽然已无影响,但会干扰调试)。
七、调试建议
排查异步问题时,先确认「这段代码属于同步、微任务还是宏任务」,再检查是否漏了 await、是否在 map 里返回了 Promise。多段异步代码先后顺序混乱时,在每段前后打时间戳日志,比盯着代码猜快得多。
小结
同步 → 微任务 → 宏任务;await 本质是微任务;循环里并发用 Promise.all,需要全部结果用 allSettled,forEach 不会等异步;fetch 超时用 AbortController。