setTimeout、Promise、async/await 的执行顺序是怎样的?
JavaScript 中的 setTimeout、Promise 和 async/await 的执行顺序主要取决于 JavaScript 的事件循环(Event Loop)机制。
简单来说,执行优先级的规则是:
同步代码 (Synchronous) > 微任务 (Microtask) > 宏任务 (Macrotask)
1. 核心概念分类
为了理解执行顺序,我们需要将代码分为三类:
同步代码 (Synchronous Code):
- 立即在调用栈(Call Stack)中执行。
- 包括:
console.log、变量赋值、new Promise()构造函数内部的代码、async函数中await之前的代码。
微任务 (Microtasks):
- 优先级高,在当前同步代码执行完后、渲染和宏任务执行前立即执行。
- 包括:
Promise.then/catch/finally、async/await(await 后面的代码)、process.nextTick(Node.js)。
宏任务 (Macrotasks):
- 优先级低,每次事件循环只取出一个执行,执行完后会再次检查微任务队列。
- 包括:
setTimeout、setInterval、setImmediate、I/O、UI 渲染。
2. 详细执行流程
事件循环的每一轮(Tick)遵循以下步骤:
- 执行栈(Call Stack):从上到下执行所有同步代码。
- 微任务队列(Microtask Queue):同步代码执行完毕后,立即清空所有微任务。
- 宏任务队列(Macrotask Queue):微任务清空后,取出一个宏任务执行。
- 循环:宏任务执行完后,再次检查微任务队列...
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'); // (假设这里还有同步代码,作为最后一行)
注意: 为了演示清晰,我调整了一下代码的阅读顺序,下面是标准的执行步骤分析:
第一步:执行同步代码
console.log('1. script start')-> 输出 "1. script start"。console.log('4. script continue')-> 输出 "4. script continue"。- 遇到
setTimeout,回调函数放入 宏任务队列。 - 执行
async1():- 输出 "2. async1 start"。
- 执行
await async2(),调用async2(),输出 "3. async2"。 async2执行完毕,await此时挂起async1的剩余部分,将console.log('6. async1 end')放入 微任务队列 (记为 Micro1)。
- 执行
new Promise:- 执行构造函数,输出 "5. promise1"。
- 执行
resolve(),将.then回调console.log('7. promise2')放入 微任务队列 (记为 Micro2)。
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)]。
- 取出 Micro1 执行 -> 输出 "6. async1 end"。
- 取出 Micro2 执行 -> 输出 "7. promise2"。
此时控制台追加输出:
plaintext
6. async1 end
7. promise2
第三步:执行宏任务
微任务清空了,去宏任务队列取出一个。
- 取出
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. 总结
记住这个口诀:
- 先跑同步:从上到下,Promise 构造函数是同步的,await 这一行是同步的。
- 看到 await:await 后面的代码丢进微任务。
- 看到 Promise.then:回调丢进微任务。
- 看到 setTimeout:回调丢进宏任务。
- 同步跑完跑微任务:所有微任务清空。
- 最后跑宏任务。