Skip to content

Promise.all 手写

手写 Promise.all 的核心不是把代码背下来,而是先理解它到底要满足什么行为。

Promise.all 的目标

可以先记成三条规则:

  • 所有项都成功,整体才成功
  • 只要有一项失败,整体立刻失败
  • 成功结果的顺序要和输入顺序一致
js
Promise.all([taskA(), taskB(), taskC()])

这里即使 taskBtaskA 先完成,最终结果数组的位置也不能乱。

手写时最关键的点

主要有四个:

  1. 返回一个新的 Promise
  2. 兼容普通值和 Promise
  3. 记录每一项的原始索引
  4. 统计还有多少项没完成

一种常见实现

js
Promise.myAll = function (iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable)
    const results = []

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

    let remaining = items.length

    items.forEach((item, index) => {
      Promise.resolve(item).then(
        (value) => {
          results[index] = value
          remaining -= 1

          if (remaining === 0) {
            resolve(results)
          }
        },
        (error) => {
          reject(error)
        },
      )
    })
  })
}

为什么要用 Promise.resolve

因为 Promise.all 不只接收 Promise,也接收普通值:

js
Promise.all([1, Promise.resolve(2), 3])

所以手写时通常要先统一包装:

js
Promise.resolve(item)

这样普通值会被转成已成功的 Promise。

为什么要记录索引

如果直接按“谁先完成就 push 到数组里”,结果顺序会错。

js
results[index] = value

这一步是为了保证输出顺序和输入顺序一致,而不是按完成顺序排列。

为什么要统计剩余数量

因为只有当全部都成功结束时,才能整体 resolve

js
let remaining = items.length

每成功一个就减一,减到 0 才说明全部完成。

为什么失败时可以直接 reject

因为原生 Promise.all 的行为就是“有一个失败就整体失败”。

js
reject(error)

之后即使其他 Promise 还在继续完成,也不会再改变整体结果。

空数组是个边界情况

js
Promise.all([])

它应该直接成功,并返回空数组:

js
resolve([])

这也是手写实现里常见的漏点。

这类题真正想考什么

通常不是考规范细节,而是考是否理解:

  • Promise 聚合模型
  • 顺序保持
  • 普通值兼容
  • 成功和失败分支

使用建议

  • 手写前先把行为规则写清楚,再开始实现。
  • 遇到“结果顺序错了”的问题,先检查是不是直接 push 了。
  • 遇到“普通值不兼容”的问题,先想到 Promise.resolve()
  • 面试手写能把核心行为实现对,通常就已经够用了。

基于 MIT 许可发布