一、前言
console.log('start')
Promise.resolve()
.then(() => {
console.log(0)
return Promise.resolve(4) // 将这里返回的 fulfilled 的 Promise 计作 myPromise
})
.then((res) => console.log(res))
Promise.resolve()
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))
.then(() => console.log(5))
console.log('end')
上面这道题刷过的都知道,核心点在于 return Promise.resolve(4) 的理解,即 当你在 .then() 中返回一个已经 fulfilled 的 Promise,到底发生了什么?为什么会推迟 2个微任务 才打印出 4 呢?
这看起来违反直觉,但这是由 Promise/A+ 规范和 JavaScript 引擎实现细节决定的。
二、核心原因
1. Promise 解析过程(Promise Resolution Procedure)
当 .then() 返回一个 thenable 对象(包括 Promise)时,JavaScript 必须执行一个特殊的解析过程:
- 注册一个微任务,用来创建一个 PromiseResolveThenableJob 来解包。
- 这个 Job 会调用返回的 Promise 的
.then()方法,因而 需要再注册 一个 值传播 的微任务 来 传递最终的结果。
即使 返回的Promise 已经完成,这个解析过程仍然会发生,因为规范没有为"已完成的 Promise"提供快捷路径。
2. 规范要求(ECMAScript 规范)
根据 PromiseResolveThenableJob 规范: 当处理程序返回 thenable 对象时,必须创建一个新的 Job 来"解包"这个 thenable
三、与直接返回值的对比
// 直接返回值
.then(() => {
return 4; // 非 thenable
})
// 执行流程:
// 微任务1: 执行回调 → 返回值4
// 微任务2: 执行下一个.then
// 返回已完成的 Promise
.then(() => {
return Promise.resolve(4); // thenable
})
// 执行流程:
// 微任务1: 执行回调 → 返回Promise
// 微任务2: PromiseResolveThenableJob
// 微任务3: 值传播任务
// 微任务4: 执行下一个.then
四、为什么规范这样设计?
-
一致性处理:
- 统一处理所有 thenable 对象(无论是否已完成)
- 避免为已完成 Promise 创建特殊路径,保持实现简单
-
处理动态值:
- Promise 可能在返回后才完成(虽然这里同步完成)
- 规范需要处理异步解决的情况
-
安全考虑:
- 防止开发者依赖同步行为
- 确保所有 Promise 行为都是异步的
五、性能影响
虽然看起来多余,但这种设计:
- 在现代 JavaScript 引擎中优化得很好
- 只增加约 0.1 微秒的开销(通常可忽略)
- 保持 Promise 行为的一致性更重要
六、浏览器实现证明
查看 V8 引擎源码(Chromium/Node.js):
// v8/src/builtins/promise-abstract-operations.tq
transitioning macro PromiseResolveThenableJob(
context: Context, promiseToResolve: JSPromise,
thenable: JSReceiver, then: JSAny): void {
// 总是创建新任务,即使 thenable 已完成
const task = new PromiseResolveThenableTask(
context, promiseToResolve, thenable, then);
EnqueueMicrotask(task);
}
七、实际代码
console.log('start --之后遇到两个resolve 的 promise,分别为其注册 微任务(即打印0 和 打印1)')
Promise.resolve()
.then(() => {
console.log(0,'--4-1-- 打印0后,由于返回一个fulfied 的 Promise,JS内部机制 会注册 微任务 PromiseResolveThenableJob 在 打印1的微任务 之后')
return Promise.resolve(4) // 将这里返回的 fulfilled 的 Promise 计作 myPromise
})
.then((res) => console.log(res))
Promise.resolve()
.then(() => console.log(1, '--4-2-- 打印1后,注册打印2,执行 PromiseResolveThenableJob,注册 传播值的微任务(该微任务会调用 myPromise.then(resolve => resolve(4)))'))
.then(() => console.log(2, '--4-3-- 打印2后,注册打印3,执行 传播值的微任务,注册 打印4的 微任务'))
.then(() => console.log(3, '--4-4-- 打印3后,注册打印5,执行 打印4的微任务,打印 4))
.then(() => console.log(5))
console.log('end')
八、 如何避免额外微任务?
如果不需要特殊处理,直接返回值:
// 高效:少2个微任务
.then(() => 2)
// 而不是:
.then(() => Promise.resolve(2))
九、总结
返回已完成的 Promise 会添加额外微任务是因为:
- 规范要求:必须通过 PromiseResolveThenableJob 处理所有 thenable
- 一致性设计:统一处理已完成/未完成的 Promise
- 安全异步:确保值传播总是异步的
- 引擎实现:V8 等引擎严格遵循规范
虽然这看起来效率不高,但这是 JavaScript Promise 设计的必要妥协,以确保在各种场景下的一致性和可靠性。