问题:
最近看到一个有意思的面试题:
js
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);
});
结论:
最终 console 输出顺序: 0, 1, 2, 3, 4, 5
原因解释
重点:Promise的处理 与 微任务队列:
解析步骤拆解:
- 脚本同步阶段:
- 两个 Promise.resolve() 都是已解决的,同步地注册各自的第一个 .then 的 reaction。它们会分别创建"返回的 promise"(记为 A_return、B_return),并把对应的
PromiseFulfillReactionJobTask(执行第一个 then 的回调)按注册顺序推入微任务队列。- 初始 微任务队列 (FIFO): [A (第一个链的第1个 then 回调), B (第二个链的第1个 then 回调)]
-
执行微任务(按队列):
-
运行 A:
- 执行回调,打印 0。
- 回调返回
Promise.resolve(4)---> 一个thenable(JSPromise)。 - 为把 A_return 与该
thenable关联,V8 创建并入队一个PromiseResolveThenableJobTask(见 src/objects/promise.tq:PromiseResolveThenableJobTask是 Microtask,携带promise_to_resolve、thenable、then等字段),用于在稍后调用thenable.then(resolve, reject)。 - 此时,微任务队列 : [B, PRJ](PRJ = PromiseResolveThenableJobTask)
-
运行 B:
- 打印 1 ,返回
undefined,导致 B_return 被 fulfill 并把 B_return.then(...)(即后续的 handler2)对应的 reaction 入队。- 此时,微任务队列 :[PRJ, B2]
- 运行 PRJ(
PromiseResolveThenableJobTask):
-
它会执行
thenable.then(resolve, reject)。因为thenable是已解决的Promise.resolve(4),调用 then 会把一个将调用resolve(4)的 reaction 入队(记为 TJ)。 -
PRJ 本身不直接调用 resolve(4),只执行 then 的调用来把 thenable 的 reaction 安排为微任务。
-
此时,微任务队列 :[B2, TJ]
- 运行 B2(第二链的第二个 then):
-
打印 2 ,返回
undefined,注册并入队 B3(下一 then 的 reaction)。 -
此时,微任务队列 :[TJ, B3]
- 运行 TJ(thenable 的反应,执行 resolve(4)):
-
resolve(4) 满足了 A_return,把 A_return 的后续 reaction(即 A 的第二个 then,也就是打印 res 的回调 A2)入队。
-
此时,微任务队列 :[B3, A2]
- 运行 B3:
-
打印 3,入队 B4(最后一个 then 的 reaction)。
-
此时,微任务队列 :[A2, B4]
- 运行 A2:打印 4
- 运行 B4:打印 5
-
要点总结:
- 当
then回调返回一个thenable(这里是Promise.resolve(4))时,规范要求通过一个PromiseResolveThenableJob(微任务)去"取得 then 并调用它",因此会生成一个专门的微任务(PRJ)来调用 then。该 PRJ 会排在当时队列的尾部。 thenable.then(resolve, reject)本身再产生一个微任务(TJ)来真正调用resolve(4)。因此 A_return 的 fulfill(A2)要等到 TJ 执行后才会被入队并最终运行。- 因为多个微任务在运行期间会继续入队(并保持 FIFO),所以第二条 promise 链的中间 then(打印 2、3)会在 A_return 的最终 reaction(打印 4)之前被调度并打印。