算法 - 手写 Promise.race

1. 题目

实现 promiseRace(iterables),行为对齐原生 Promise.race

  • 接收一个可迭代的 Promise(或普通值)集合,返回一个新 Promise。
  • 结果由**第一个 settle(无论 fulfilled 还是 rejected)**的成员决定:它成功就 resolve,它失败就 reject。
  • 关键区别于 Promise.all:race 不关心成败,只关心谁先落地
  • 空数组:原生 Promise.race([])永远 pending(没有任何成员能触发 settle),需照此实现。

示例:

1
2
promiseRace([慢成功, 快失败])  -> reject(快的那个先 settle)
promiseRace([快成功, 慢失败]) -> resolve(快的那个先 settle)

2. 解题思路

一句话:给每个成员同时挂上 resolve 和 reject,谁先调用谁就赢

因为 Promise 一旦 settle 就冻结,后续再调用 resolve/reject 都是空操作。所以完全不必自己加「是否已完成」的锁:

  1. 遍历集合,用 Promise.resolve(item) 包一层(兼容非 Promise 值,让它也进入微任务队列公平竞争)。
  2. 对每个包装后的 Promise,.then(resolve, reject)——把外层的 resolve、reject 直接透传。
  3. 第一个 settle 的成员会率先调用外层 resolve 或 reject,外层 Promise 随即定型,其余调用被忽略。
  • 空数组直接返回一个不 settle 的 Promise(new 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
function promiseRace<T>(
iterables: Iterable<T | PromiseLike<T>>
): Promise<Awaited<T>> {
return new Promise((resolve, reject) => {
for (const item of iterables) {
// 用 Promise.resolve 包装,普通值也异步参与竞争
Promise.resolve(item).then(resolve, reject);
}
// 空集合:循环不执行,Promise 永久 pending(与原生一致)
});
}

// 测试
const slow = (v: string, ms: number) =>
new Promise<string>((r) => setTimeout(() => r(v), ms));
const fastFail = (ms: number) =>
new Promise<never>((_, rej) => setTimeout(() => rej("boom"), ms));

promiseRace([slow("slow-win", 300), fastFail(100)])
.then(console.log)
.catch(console.log); // "boom"(先 settle)

promiseRace([slow("first", 50), slow("second", 200)])
.then(console.log); // "first"

4. 面试延伸

  • race vs anyPromise.any 只在第一个成功时定胜负,忽略失败;只有全部失败才 reject 一个 AggregateError。别把 race 的「先 settle」和 any 的「先成功」搞混。
  • race vs allSettled:allSettled 等所有人落地并汇总状态,永不 reject;race 恰恰相反,抢跑即定。
  • 实战用法请求超时控制是 race 最典型场景——Promise.race([fetchData(), timeout(5000)]),超时的那个先 reject 就达成「N 秒没返回就报错」。
  • 能主动说出「空数组永远 pending」这个反直觉细节,是加分信号,说明你真用过而非只背签名。

难度:中等 | 手写 Promise 家族 | 超时控制基石