JS await 执行顺序
· 4 min read
JavaScript 中 await 的执行顺序,是面试高频题,也是写异步代码最容易踩的坑。
await后面的表达式 立即同步执行,只有等号左边的赋值才被推入微任务队列await本质是Promise.then的语法糖,每个await至少消耗 一 个微任务 tick- 循环里
await串行执行,N 个请求耗时 N 倍,必须用Promise.all并发 - 同步代码 → 微任务(await/then)→ 宏任务(setTimeout)的顺序永远不变
- 判断输出顺序的口诀:先跑同步,再清微任务,最后才轮到宏任务
await 到底干了什么
很多人以为 await 就是"等一下",其实它是 Promise.then 的语法糖。下面这两段代码几乎等价:
async function foo() {
const x = await bar();
console.log(x);
}
function foo() {
return bar().then(x => {
console.log(x);
});
}
关键点:await bar() 中,bar() 是立即同步执行的,只有拿到结果之后的赋值和后续代码才会被包进 .then 回调,扔到微任务队列里。
一道经典面试题
async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}
console.log('4');
async1();
console.log('5');
输出顺序是:4 → 1 → 3 → 5 → 2。
执行过程拆解:
console.log('4')同步执行,输出4- 调用
async1(),进入函数体输出1 - 遇到
await async2(),async2()同步执行输出3,然后把console.log('2')包成微任务挂起 async1暂停,控制权回到主线程,执行console.log('5')输出5- 主线程同步代码跑完,清空微任务队列,输出
2
记住一句话:await 之后的代码相当于写在 .then 里。
循环里的 await 是性能杀手
下面这段代码看着没问题,实际是个灾难:
// 错误:3 个请求串行,总耗时 3 倍
for (const id of ids) {
const data = await fetch(`/api/${id}`);
results.push(data);
}
每个 await 都会等上一个 Promise resolve 才发下一个请求。如果有 100 个 id,单个请求 200ms,总耗时就是 20 秒。
正确写法是用 Promise.all 并发:
// 正确:3 个请求并发,总耗时取决于最慢那个
const results = await Promise.all(
ids.map(id => fetch(`/api/${id}`))
);
注意:Promise.all 一旦有一个 reject 整体就挂了。需要部分容错用 Promise.allSettled。
微任务 vs 宏任务
异步任务分两类:
- 微任务:
Promise.then、await、queueMicrotask、MutationObserver - 宏任务:
setTimeout、setInterval、I/O、UI 渲染、setImmediate(Node)