为何Promise.race()回调晚于Promise.resolve().then()执行?求官方/源码级解释
为何Promise.race()回调晚于Promise.resolve().then()执行?求官方/源码级解释
您好!这个问题的核心在于Promise.race的内部执行逻辑和微任务队列的调度顺序,我会从规范到V8源码层面给你拆解清楚~
一、先理清代码的完整执行流程
我们一步步还原代码的执行顺序,明确同步任务和微任务的生成时机,就能明白为什么输出顺序是这样的:
1. 同步代码执行阶段
console.log('script start') // 输出: script start // 实例化promise1:Promise构造函数是同步执行的 const promise1 = new Promise((resolve, reject) => { console.log('promise1') // 输出: promise1 resolve("A") // promise1立即变为fulfilled状态 }) // 实例化promise2:逻辑和promise1完全一致 const promise2 = new Promise((resolve, reject) => { console.log('promise2') // 输出: promise2 resolve("B") // promise2立即变为fulfilled状态 }) // 重点:执行Promise.race const p = Promise.race([promise1, promise2]) .then((value) => { console.log('race:', value) }) // Promise.race的内部逻辑: // 它会给数组里的每个Promise注册一个then回调——只要有一个Promise resolve,就用这个值resolve race返回的promise p // 因为promise1已经是fulfilled状态,这个注册的then回调会被直接加入**微任务队列(我们叫它T1)** // 执行Promise.resolve().then(...) Promise.resolve().then(()=> { console.log('promise3') // 这个回调直接加入微任务队列(叫它T2) }) console.log('script end') // 输出: script end
此时同步代码执行完毕,微任务队列的顺序是:[T1, T2]
2. 微任务队列处理阶段
事件循环处理微任务队列严格遵循**先进先出(FIFO)**的规则:
- 先执行T1:这是Promise.race内部给promise1注册的回调,执行后会把race返回的promise p标记为fulfilled。这时p的then回调(就是你写的
console.log('race:', value))才会被加入微任务队列末尾(叫它T3),此时队列变成[T2, T3] - 接着执行T2:输出
promise3,队列剩下[T3] - 最后执行T3:输出
race: A
这就完美对应了你看到的输出顺序!
二、ECMAScript 规范层面的官方解释
根据ECMAScript 2024规范中Promise.race的定义,核心规则是:
- 遍历输入的可迭代对象,为每个元素调用
Promise.resolve(这里promise1已经是Promise,直接复用) - 为每个处理后的Promise注册
then回调:只要任意一个Promise状态变为fulfilled/rejected,就同步修改Promise.race返回的Promise状态 - 关键:即使目标Promise已经是fulfilled状态,给它注册的
then回调也必须放入微任务队列异步执行——规范不允许同步触发Promise的回调,所有Promise回调都必须走微任务。
而Promise.resolve().then(...)的逻辑是直接把回调放入微任务队列,不需要额外的中间步骤,所以它的回调会先于Promise.race的最终回调执行。
三、V8引擎源码级验证
以你使用的Chrome 131对应的V8 131版本为例,我们看具体实现:
- 在
src/builtins/promise-race.tq中,Promise.race的核心逻辑是遍历数组,为每个Promise调用ThenableThen方法注册回调:// 简化后的核心逻辑 for (const promise of iterable) { const thenable = Cast<Thenable>(promise); ThenableThen( thenable, context, // Promise resolve时的回调:标记race返回的Promise为fulfilled (value) => ResolvePromise(racePromise, value), (reason) => RejectPromise(racePromise, reason) ); } ThenableThen会检查Promise状态:如果已经是fulfilled,就把传入的回调包装成微任务,加入V8的MicrotaskQueue- 而
Promise.resolve().then(...)的回调会直接通过EnqueueMicrotask加入队列,没有中间环节。
所以微任务队列的顺序是:T1(race的中间回调)→ T2(promise3的回调)。T1执行后才会把race的最终回调T3加入队列,此时T2已经在队列里等着了,自然先输出promise3。
总结
本质原因是Promise.race的最终回调需要一个中间微任务来触发它返回的Promise的resolve,而Promise.resolve().then(...)的回调是直接入队的。同步代码结束后,中间微任务先执行,但它只是“解锁”了race的最终回调,此时race的回调才入队,排在promise3的回调之后,所以输出顺序是promise3在前,race: A在后。
备注:内容来源于stack exchange,提问作者jinmokai
相关产品推荐
相关产品推荐

