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)
每次主线程同步代码跑完后,引擎会一次性清空整个微任务队列,然后才取出一个宏任务执行。这就是为什么下面这段会先输出 promise 再输出 timeout:
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// 输出:sync → promise → timeout
哪怕 setTimeout 的延迟是 0,它也是宏任务,要排在所有微任务后面。
一个反直觉的细节
await 后面跟非 Promise 值会怎样?
async function f() {
await 1; // 等价于 await Promise.resolve(1)
console.log('a');
}
引擎会自动用 Promise.resolve 包一层,所以哪怕 await 1 这种看似无意义的写法,也会消耗一个微任务 tick。这意味着 await 之后的代码永远不会和当前同步代码同时执行,哪怕等的是一个常量。
实战建议
- 不需要顺序依赖时,永远不要在循环里用
await,改用Promise.all - 用
try/catch包await,比.catch()链式调用更清晰 - 顶层
await只在 ES Module 里能用,CJS 报错 - 调试异步顺序时,把
await心里翻译成.then,逻辑就清楚了