Skip to main content

JS await 执行顺序

· 4 min read

JavaScript 中 await 的执行顺序,是面试高频题,也是写异步代码最容易踩的坑。

  1. await 后面的表达式 立即同步执行,只有等号左边的赋值才被推入微任务队列
  2. await 本质是 Promise.then 的语法糖,每个 await 至少消耗 个微任务 tick
  3. 循环里 await 串行执行,N 个请求耗时 N 倍,必须用 Promise.all 并发
  4. 同步代码 → 微任务(await/then)→ 宏任务(setTimeout)的顺序永远不变
  5. 判断输出顺序的口诀:先跑同步,再清微任务,最后才轮到宏任务

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

执行过程拆解:

  1. console.log('4') 同步执行,输出 4
  2. 调用 async1(),进入函数体输出 1
  3. 遇到 await async2()async2() 同步执行输出 3,然后把 console.log('2') 包成微任务挂起
  4. async1 暂停,控制权回到主线程,执行 console.log('5') 输出 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.thenawaitqueueMicrotaskMutationObserver
  • 宏任务setTimeoutsetInterval、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 之后的代码永远不会和当前同步代码同时执行,哪怕等的是一个常量。

实战建议

  1. 不需要顺序依赖时,永远不要在循环里用 await,改用 Promise.all
  2. try/catchawait,比 .catch() 链式调用更清晰
  3. 顶层 await 只在 ES Module 里能用,CJS 报错
  4. 调试异步顺序时,把 await 心里翻译成 .then,逻辑就清楚了

Read More