当 Promise 的 then 链中 return Promise.resolve (value),会发生什么?

169 阅读3分钟

一、前言

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

四、为什么规范这样设计?

  1. 一致性处理

    • 统一处理所有 thenable 对象(无论是否已完成)
    • 避免为已完成 Promise 创建特殊路径,保持实现简单
  2. 处理动态值

    • Promise 可能在返回后才完成(虽然这里同步完成)
    • 规范需要处理异步解决的情况
  3. 安全考虑

    • 防止开发者依赖同步行为
    • 确保所有 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 会添加额外微任务是因为:

  1. 规范要求:必须通过 PromiseResolveThenableJob 处理所有 thenable
  2. 一致性设计:统一处理已完成/未完成的 Promise
  3. 安全异步:确保值传播总是异步的
  4. 引擎实现:V8 等引擎严格遵循规范

虽然这看起来效率不高,但这是 JavaScript Promise 设计的必要妥协,以确保在各种场景下的一致性和可靠性。