基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

setTimeout、Promise、async/await 的执行顺序是怎样的?

知识点图片

JavaScript 中的 setTimeoutPromiseasync/await 的执行顺序主要取决于 JavaScript 的事件循环(Event Loop)机制

简单来说,执行优先级的规则是:
同步代码 (Synchronous) > 微任务 (Microtask) > 宏任务 (Macrotask)

1. 核心概念分类

为了理解执行顺序,我们需要将代码分为三类:

  1. 同步代码 (Synchronous Code)

    • 立即在调用栈(Call Stack)中执行。
    • 包括:console.log、变量赋值、new Promise() 构造函数内部的代码、async 函数中 await 之前的代码。
  2. 微任务 (Microtasks)

    • 优先级高,在当前同步代码执行完后、渲染和宏任务执行前立即执行。
    • 包括:Promise.then/catch/finallyasync/await (await 后面的代码)、process.nextTick (Node.js)。
  3. 宏任务 (Macrotasks)

    • 优先级低,每次事件循环只取出一个执行,执行完后会再次检查微任务队列。
    • 包括:setTimeoutsetIntervalsetImmediate、I/O、UI 渲染。

2. 详细执行流程

事件循环的每一轮(Tick)遵循以下步骤:

  1. 执行栈(Call Stack):从上到下执行所有同步代码
  2. 微任务队列(Microtask Queue):同步代码执行完毕后,立即清空所有微任务。
  3. 宏任务队列(Macrotask Queue):微任务清空后,取出一个宏任务执行。
  4. 循环:宏任务执行完后,再次检查微任务队列...

3. async/await 的特殊说明

async/await 是 Promise 的语法糖,它的转换逻辑如下:

  • async 函数被调用时,它会立即执行(同步),直到遇到第一个 await
  • await 右侧的表达式会立即执行。
  • 关键点await 下面的代码会被阻塞,实际上是被放入了 微任务队列 中(相当于 Promise.then 里的代码),等待当前同步代码执行完毕后才恢复执行。

4. 经典面试题解析

通过一段代码来彻底搞懂它们的顺序:

javascript
console.log('1. script start'); // 同步

async function async1() {
  console.log('2. async1 start'); // 同步
  await async2(); 
  // await 后面的代码进入微任务队列 [微任务1]
  console.log('6. async1 end'); 
}

async function async2() {
  console.log('3. async2'); // 同步
}

console.log('4. script continue'); // 同步

setTimeout(function() {
  // 进入宏任务队列 [宏任务1]
  console.log('8. setTimeout'); 
}, 0);

async1();

new Promise(function(resolve) {
  console.log('5. promise1'); // 同步 (构造函数立即执行)
  resolve();
}).then(function() {
  // 进入微任务队列 [微任务2]
  console.log('7. promise2'); 
});

console.log('script end'); // (假设这里还有同步代码,作为最后一行)

注意: 为了演示清晰,我调整了一下代码的阅读顺序,下面是标准的执行步骤分析

第一步:执行同步代码

  1. console.log('1. script start') -> 输出 "1. script start"
  2. console.log('4. script continue') -> 输出 "4. script continue"
  3. 遇到 setTimeout,回调函数放入 宏任务队列
  4. 执行 async1()
    • 输出 "2. async1 start"
    • 执行 await async2(),调用 async2(),输出 "3. async2"
    • async2 执行完毕,await 此时挂起 async1 的剩余部分,将 console.log('6. async1 end') 放入 微任务队列 (记为 Micro1)。
  5. 执行 new Promise
    • 执行构造函数,输出 "5. promise1"
    • 执行 resolve(),将 .then 回调 console.log('7. promise2') 放入 微任务队列 (记为 Micro2)。
  6. console.log('script end') (假设有) -> 输出 "script end"

此时控制台输出:

plaintext
1. script start
4. script continue
2. async1 start
3. async2
5. promise1
script end

第二步:清空微任务队列

此时宏任务队列有 [setTimeout],微任务队列有 [Micro1(async1 end), Micro2(promise2)]

  1. 取出 Micro1 执行 -> 输出 "6. async1 end"
  2. 取出 Micro2 执行 -> 输出 "7. promise2"

此时控制台追加输出:

plaintext
6. async1 end
7. promise2

第三步:执行宏任务

微任务清空了,去宏任务队列取出一个。

  1. 取出 setTimeout 回调执行 -> 输出 "8. setTimeout"

最终完整输出顺序:

plaintext
1. script start
4. script continue
2. async1 start
3. async2
5. promise1
script end
6. async1 end
7. promise2
8. setTimeout

5. 总结

记住这个口诀:

  1. 先跑同步:从上到下,Promise 构造函数是同步的,await 这一行是同步的。
  2. 看到 await:await 后面的代码丢进微任务
  3. 看到 Promise.then:回调丢进微任务
  4. 看到 setTimeout:回调丢进宏任务
  5. 同步跑完跑微任务:所有微任务清空。
  6. 最后跑宏任务
00:00
00:00