算法 - 手写 Promise.all

1. 题目

实现一个函数 promiseAll(iterables),行为对齐原生 Promise.all:

  • 接收一个(Promise 或普通值可迭代)数组,返回一个新 Promise。
  • 全部成功时,新 Promise 以「与输入顺序一致」的结果数组 resolve。
  • 任意一个失败时,立即 reject 第一个失败的原因(快速失败)。
  • 要能处理非 Promise 元素(普通值直接当作已成功)。
  • 空数组应立即 resolve 为 []。

示例:

1
2
3
4
5
promiseAll([1, Promise.resolve(2), fetchUrl])
-> resolve 为 [1, 2, <fetch结果>](顺序保持)

promiseAll([ok1, fail, ok2])
-> reject(fail 的原因)

2. 解题思路

三个关键点:

  1. 顺序保持:结果数组按输入下标写入,不能用 push(谁先完成谁 push 会乱序)。给每个任务捕获其 index,完成时 results[index] = value。
  2. 完成计数:用一个计数器 completed,每当一个任务成功就 +1;当 completed === 总长度 时 resolve(results)。因为异步完成顺序不定,只能靠计数判断「全好了」。
  3. 快速失败:在任一任务 catch 到错误时,直接 reject(reason)。Promise 一旦 settle 就冻结,后续再 reject 无效,所以无需额外加锁,天然只 reject 第一次。

用 Promise.resolve(x).then(...) 包裹每个元素,可同时兼容「普通值」和「Promise」——普通值会被包装成已完成的 Promise。

  • 时间复杂度:O(n)。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function promiseAll<T>(
iterables: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>[]> {
return new Promise((resolve, reject) => {
const promises = Array.from(iterables);
const n = promises.length;
const results = new Array(n);
let completed = 0;

if (n === 0) {
resolve(results as Awaited<T>[]);
return;
}

for (let i = 0; i < n; i++) {
// 用 Promise.resolve 兼容非 Promise 值
Promise.resolve(promises[i]).then(
(value) => {
results[i] = value; // 按下标写入,保证顺序
completed++;
if (completed === n) {
resolve(results as Awaited<T>[]);
}
},
(reason) => {
reject(reason); // 快速失败
}
);
}
});
}

// 测试
promiseAll([1, Promise.resolve(2), new Promise((r) => setTimeout(() => r(3), 100))])
.then(console.log); // [1, 2, 3]

promiseAll([Promise.resolve("a"), Promise.reject("boom"), Promise.resolve("c")])
.catch(console.log); // "boom"

4. 面试延伸

手写 Promise 家族常一起问,趁热记住区别:

  • Promise.allSettled:等全部 settle(不管成败),返回 {status, value/reason} 数组,永不 reject。
  • Promise.race:第一个 settle(含 reject)的结果定胜负。
  • Promise.any:第一个成功的结果;全部失败才 reject 一个 AggregateError。
  • 并发控制(重点):真实场景不能无脑并发几千个请求。进阶手写 promiseAllLimit(tasks, limit),用「任务池 / 索引指针」维持最多 limit 个在跑,是字节/腾讯高频加试题。
  • 别忘了答「为什么按 index 写而不是 push」——这几乎是本题的核心得分点。

难度:中等 | 前端手写 Promise 必考 | 异步并发基石