You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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的定义,核心规则是:

  1. 遍历输入的可迭代对象,为每个元素调用Promise.resolve(这里promise1已经是Promise,直接复用)
  2. 为每个处理后的Promise注册then回调:只要任意一个Promise状态变为fulfilled/rejected,就同步修改Promise.race返回的Promise状态
  3. 关键:即使目标Promise已经是fulfilled状态,给它注册的then回调也必须放入微任务队列异步执行——规范不允许同步触发Promise的回调,所有Promise回调都必须走微任务。

而Promise.resolve().then(...)的逻辑是直接把回调放入微任务队列,不需要额外的中间步骤,所以它的回调会先于Promise.race的最终回调执行。

三、V8引擎源码级验证

以你使用的Chrome 131对应的V8 131版本为例,我们看具体实现:

  1. 在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)
      );
    }
    
  2. ThenableThen会检查Promise状态:如果已经是fulfilled,就把传入的回调包装成微任务,加入V8的MicrotaskQueue
  3. 而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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 13:39:32