引言
在 JavaScript 异步编程中,Promise 的微任务调度机制是一个重要且容易混淆的概念。本文将通过一个具体的代码示例,深入分析 Promise 微任务的执行顺序,特别是当 .then() 方法返回另一个 Promise 时的特殊行为。
示例代码
javascript
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(6)
})
输出结果
text
0
1
2
3
4
5
6
核心概念
微任务队列
- Promise 回调属于微任务(Microtask)
- 微任务在当前宏任务执行完毕后、下一个宏任务开始前执行
- 微任务队列遵循先进先出(FIFO)原则
Promise 链的特殊规则
当 .then() 方法返回一个 Promise 时,根据 ECMAScript 规范,会触发额外的微任务调度:
- 第一个微任务:PromiseResolveThenableJob
- 第二个微任务:实际传递解决值
详细执行过程分析
阶段一:初始状态
javascript
// 微任务队列初始状态
微任务队列: [A1, B1]
- A1: 第一个 Promise 链的第一个 .then()
- B1: 第二个 Promise 链的第一个 .then()
阶段二:第一轮执行
执行 A1:
javascript
console.log(0) // 输出: 0
return Promise.resolve(4) // 返回 Promise
// 创建一个微任务 X1 (PromiseResolveThenableJob)
执行 B1:
javascript
console.log(1) // 输出: 1
// 返回 undefined, B2 进入微任务队列
此时微任务队列: [X1, B2]
阶段三:处理返回的 Promise
执行 X1:
javascript
// 处理 Promise.resolve(4) 的解决
// 创建微任务 X2 (传递解决值)
此时微任务队列: [B2, X2]
阶段四:继续执行
执行 B2:
javascript
console.log(2) // 输出: 2
// 返回 undefined, B3 进入队列
此时微任务队列: [X2, B3]
执行 X2:
javascript
// 将第一个链的第二个 .then() (A2) 放入队列
此时微任务队列: [B3, A2]
阶段五:交替执行
执行 B3:
javascript
console.log(3) // 输出: 3
// 返回 undefined, B4 进入队列
此时微任务队列: [A2, B4]
执行 A2:
javascript
console.log(4) // 输出: 4
// 第一个 Promise 链结束
此时微任务队列: [B4]
阶段六:收尾执行
执行 B4:
javascript
console.log(5) // 输出: 5
// 返回 undefined, B5 进入队列
此时微任务队列: [B5]
执行 B5:
javascript
console.log(6) // 输出: 6
关键机制解析
为什么 return Promise.resolve(4) 会导致延迟?
当 .then() 返回一个 Promise 时,需要等待这个 Promise 完全解决。根据 ECMAScript 规范第 25.4.2.1 和 25.4.2.2 节:
- PromiseResolveThenableJob: 第一个微任务,用于调用 Promise 的
then方法 - 传递解决值: 第二个微任务,用于将解决值传递给后续的
.then()
微任务队列可视化
text
时间轴:
t0: [A1, B1]
t1: 执行 A1 → 输出 0, 创建 X1
t2: 执行 B1 → 输出 1, 创建 B2
t3: 队列变为 [X1, B2]
t4: 执行 X1 → 创建 X2
t5: 队列变为 [B2, X2]
t6: 执行 B2 → 输出 2, 创建 B3
t7: 队列变为 [X2, B3]
t8: 执行 X2 → 创建 A2
t9: 队列变为 [B3, A2]
t10: 执行 B3 → 输出 3, 创建 B4
t11: 队列变为 [A2, B4]
t12: 执行 A2 → 输出 4
t13: 队列变为 [B4]
t14: 执行 B4 → 输出 5, 创建 B5
t15: 队列变为 [B5]
t16: 执行 B5 → 输出 6
常见误解澄清
误解一:"所有输出都是顺序的"
- 错误原因:认为 Promise 链会完全执行完再执行下一个
- 实际情况:微任务队列交替执行两个 Promise 链的任务
实践建议
1. 避免在 .then() 中返回不必要的 Promise
javascript
// 不推荐
.then(() => {
return Promise.resolve(someValue)
})
// 推荐(如果不需要异步处理)
.then(() => {
return someValue
})
2. 理解微任务延迟
当遇到异步操作顺序问题时,考虑:
.then()是否返回了 Promise?- 返回的 Promise 是否需要额外的微任务处理?
3. 使用 async/await 提高可读性
javascript
async function example() {
console.log(0)
await Promise.resolve()
const res = await Promise.resolve(4)
console.log(res)
}
async function example2() {
console.log(1)
await Promise.resolve()
console.log(2)
await Promise.resolve()
console.log(3)
await Promise.resolve()
console.log(5)
await Promise.resolve()
console.log(6)
}
浏览器兼容性说明
不同 JavaScript 引擎在 Promise 微任务调度上可能存在差异:
- V8 (Chrome/Node.js): 遵循最新 ECMAScript 规范
- SpiderMonkey (Firefox): 可能有细微差异
- JavaScriptCore (Safari): 实现可能略有不同
总结
Promise 的微任务调度机制是 JavaScript 异步编程的核心。理解 return Promise.resolve() 会引入额外微任务这一特性,对于调试复杂的异步代码至关重要。通过本文的分析,我们可以看到:
- 微任务队列管理着所有 Promise 回调的执行顺序
- 返回 Promise 的
.then()需要两个额外微任务来处理 - 多个 Promise 链会交替执行,而不是一个链完全执行完再执行另一个
掌握这些细节有助于编写更可靠、可预测的异步 JavaScript 代码。
面试真题
根据下面的代码给出输出顺序,并解释
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
最终的输出顺序为:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
执行流程分析
1. 同步执行阶段(调用栈)
| 执行顺序 | 代码 | 输出 | 说明 |
|---|---|---|---|
| 1 | console.log('script start') | script start | 同步代码立即执行 |
| 2 | setTimeout(...) | - | 将回调函数添加到宏任务队列 |
| 3 | async1() 调用 | - | 进入 async1 函数 |
| 3.1 | console.log('async1 start') | async1 start | async1 中的同步代码 |
| 3.2 | await async2() | - | 调用 async2 函数 |
| 3.2.1 | console.log('async2') | async2 | async2 中的同步代码 |
| 3.3 | await 后的代码 | - | 被转换为微任务,添加到微任务队列 |
| 4 | new Promise(...) | - | 创建 Promise 实例 |
| 4.1 | console.log('promise1') | promise1 | Promise 构造函数的同步代码 |
| 4.2 | resolve() | - | 立即解析 Promise |
| 4.3 | .then(...) | - | 将回调函数添加到微任务队列 |
| 5 | console.log('script end') | script end | 最后的同步代码 |
2. 微任务执行阶段
同步代码执行完毕后,事件循环开始处理微任务队列:
| 执行顺序 | 微任务来源 | 输出 | 说明 |
|---|---|---|---|
| 1 | await async2() 后的代码 | async1 end | async1 函数中 await 后面的代码 |
| 2 | Promise 的 .then() 回调 | promise2 | Promise 解析后的回调函数 |
3. 宏任务执行阶段
微任务队列清空后,事件循环处理宏任务队列:
| 执行顺序 | 宏任务来源 | 输出 | 说明 |
|---|---|---|---|
| 1 | setTimeout 回调 | setTimeout | 定时器回调函数 |
关键概念解释
事件循环 (Event Loop)
JavaScript 使用单线程事件循环模型处理异步操作,主要包括:
- 调用栈 (Call Stack) :执行同步代码
- 微任务队列 (Microtask Queue) :Promise 回调、async/await 转换的代码
- 宏任务队列 (Macrotask Queue) :setTimeout、setInterval、I/O 操作等
执行优先级
- 同步代码(调用栈)
- 所有微任务(直到微任务队列为空)
- 一个宏任务
- 重复步骤 2-3
async/await 转换
javascript
// 原始代码
await async2();
console.log('async1 end');
// 近似转换为
async2().then(() => {
console.log('async1 end');
});
最终输出顺序
text
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
内存与队列状态变化
初始状态
- 调用栈:[全局上下文]
- 微任务队列:[]
- 宏任务队列:[]
同步代码执行后
- 调用栈:[]
- 微任务队列:[
async1 end回调,promise2回调] - 宏任务队列:[
setTimeout回调]
微任务执行后
- 调用栈:[]
- 微任务队列:[]
- 宏任务队列:[
setTimeout回调]
宏任务执行后
- 调用栈:[]
- 微任务队列:[]
- 宏任务队列:[]
常见陷阱与注意事项
- Promise 构造函数是同步执行的,只有
.then()、.catch()和.finally()的回调是异步的 - await 会暂停当前 async 函数的执行,将后续代码作为微任务
- 微任务在下一轮事件循环前执行,宏任务在下一轮事件循环执行
- 多个微任务会一次性全部执行,直到微任务队列为空