← 返回题目列表

如何手写 Promise.race、allSettled 和 any?

高频 困难 第 15 / 27 题 更新于 2026/07/29
手写代码Promise异步Polyfill

简化版

race 谁先落定就跟谁走;allSettled 等所有项结束并返回状态明细;any 谁先成功就成功,全部失败才失败。手写时要处理普通值、空输入、结果顺序和失败收集,不能把三者都写成 Promise.all 的变体。

详细版

三者都可以先把 iterable 转成数组,再用 Promise.resolve 统一包装每一项。race 对每一项同时注册 resolve/rejectallSettled 无论成功失败都写入 { status, value/reason }any 成功就 resolve,失败则计数并收集原因,直到全部失败。

function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    for (const item of iterable) {
      Promise.resolve(item).then(resolve, reject)
    }
  })
}

真正的区分点在“短路条件”:race 是最先 settled,any 是最先 fulfilled,allSettled 完全不短路。空输入也不同:race([]) 永远 pending,allSettled([]) 成功为 []any([]) 会拒绝。

完整版教学

一、先用状态表分清三兄弟

API成功条件失败条件是否保留每项状态
race第一个 fulfilled第一个 rejected不保留
allSettled所有项 settled不会因单项失败而失败保留
any第一个 fulfilled所有项 rejected只在全失败时收集原因

这三个 API 都接收 iterable,但表达的业务语义完全不同。race 适合超时竞速,allSettled 适合批量任务结果看板,any 适合多个备选源中取第一个可用结果。

易错点:race 的“先”看 settled,失败也算;any 的“先”只看成功,失败会先被记下来。

二、手写 race

function promiseRace(iterable) {
  return new Promise((resolve, reject) => {
    for (const item of iterable) {
      Promise.resolve(item).then(resolve, reject)
    }
  })
}

如果输入为空,循环体不会执行,返回的 Promise 会保持 pending。这个行为常用于“永不完成”的占位 Promise,但业务代码里要小心,否则等待链路可能一直挂住。

时间线上看,A 在 80ms 成功,B 在 30ms 失败,结果一定是 B 的失败:

0ms:  race([A, B])
30ms: B rejected -> overall rejected
80ms: A fulfilled -> 已无影响

三、手写 allSettled

function promiseAllSettled(iterable) {
  return new Promise(resolve => {
    const items = Array.from(iterable)
    const results = new Array(items.length)
    let settledCount = 0

    if (items.length === 0) {
      resolve([])
      return
    }

    items.forEach((item, index) => {
      Promise.resolve(item).then(
        value => {
          results[index] = { status: 'fulfilled', value }
        },
        reason => {
          results[index] = { status: 'rejected', reason }
        },
      ).then(() => {
        settledCount += 1
        if (settledCount === items.length) resolve(results)
      })
    })
  })
}

allSettled 的关键是“吞掉单项失败并记录”。如果在失败分支里继续 reject 整体 Promise,它就退化成了 all,无法给出完整明细。

四、手写 any

function promiseAny(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable)
    const errors = new Array(items.length)
    let rejectedCount = 0

    if (items.length === 0) {
      reject(new AggregateError([], 'All promises were rejected'))
      return
    }

    items.forEach((item, index) => {
      Promise.resolve(item).then(
        resolve,
        reason => {
          errors[index] = reason
          rejectedCount += 1
          if (rejectedCount === items.length) {
            reject(new AggregateError(errors, 'All promises were rejected'))
          }
        },
      )
    })
  })
}

any 的失败不是第一个失败,而是全部失败。假设 3 个源分别在 10ms、20ms 失败,50ms 成功,最终仍然成功,前两个失败只作为过程中间状态。

五、空输入和普通值边界

输入raceallSettledany
[]pendingfulfilled []rejected AggregateError
[1]fulfilled 1[{status:'fulfilled', value:1}]fulfilled 1
[Promise.reject('e')]rejected 'e'rejected 状态对象rejected AggregateError

普通值通过 Promise.resolve 视为已完成值,所以它会在微任务中参与竞争。面试时经常会问 race([1, delayed]),结果应很快完成为 1

六、典型应用:请求超时

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('timeout')), ms)
  })
}

promiseRace([fetch('/api/user'), timeout(3000)])

这段代码只能让外层 Promise 变成超时失败,并不会自动中止 fetch。生产代码若要真的停止网络请求,应结合 AbortController。这和 Promise.all 的短路但不取消是同一类边界。

七、常见误区与追问

  • 误区:race 只取第一个成功。 它取第一个 settled,失败也会让整体失败。
  • 误区:allSettled 会 reject。 单项失败会被包装成结果对象,整体通常 fulfilled。
  • 误区:anyrace 一样。 any 忽略单项失败,直到全部失败才拒绝。
  • 追问:为什么 any([]) 要失败? 没有任何候选能成功,所以结果是聚合失败。
  • 追问:AggregateError 里应该放什么? 放每个失败原因,通常按输入顺序保存。
  • 追问:超时竞速会取消慢请求吗? 不会,取消需要底层 API 支持。

八、加强记忆

把三者记成三个裁判:race 是最急的裁判,谁先有结果就吹哨;allSettled 是登记员,等所有人交卷再给状态表;any 是容错裁判,只要有人成功就放行,所有人都失败才判失败。手写时围绕短路条件、空输入和结果顺序展开,基本不会偏。