0%

手写 Promise.race(从原理到实战)

在前端面试和日常业务中,Promise.race 是一个非常常见但又容易被忽略的 API。它的规则很简单:

多个 Promise 同时“赛跑”,谁先落地(fulfilled 或 rejected),整体 Promise 就跟着落地。

这篇文章我们会:

  • 先讲清楚 Promise.race 的行为与典型使用场景
  • 再手写一个 myPromiseRace
  • 最后补充边界情况、优化点与面试常考问题

1. Promise.race 是什么?

Promise.race(iterable) 接收一个可迭代对象(通常是数组),返回一个新的 Promise:

  • 只要 任意一个 Promise 先 fulfilled,结果 Promise 就 fulfilled
  • 只要 任意一个 Promise 先 rejected,结果 Promise 就 rejected
  • 它不会等待全部结束,也不会“取消”其他 Promise(它们仍然会继续执行)

2. 常见使用场景

2.1 超时控制(最常见)

比如请求接口超过 3 秒就当作超时处理:

  • fetch(url) vs timeoutPromise(3000)
  • 谁先结束就用谁的结果

2.2 多源请求兜底

比如同一个资源从 CDN1/CDN2 同时拉取,谁先成功就用谁。

2.3 首屏竞速

首屏数据、降级数据、缓存数据同时读取,谁先返回就先渲染。

3. 手写 myPromiseRace

你这版实现非常接近原生行为:

  • 校验 iterable
  • 遍历输入
  • Promise.resolve 兼容非 Promise 值
  • 谁先 settle 就 resolve/reject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
// 检查输入是否为可迭代对象
if (!promises || typeof promises[Symbol.iterator] !== 'function') {
return reject(new TypeError('Argument is not iterable'));
}

// 遍历每个 Promise / 值
for (const promise of promises) {
// 用 Promise.resolve 包一层,确保 thenable / 普通值都能被处理
Promise.resolve(promise)
.then(resolve) // 谁先 fulfilled,整体就 fulfilled
.catch(reject); // 谁先 rejected,整体就 rejected
}
});
}

4. 关键点拆解(为什么要 Promise.resolve?)

Promise.race 的参数里不一定都是 Promise,可能是:

  • 普通值:Promise.race([1, 2, 3])
  • thenable:{ then(resolve) { resolve(123) } }

使用 Promise.resolve(x) 的好处是:

  • 普通值会立刻变成 fulfilled Promise
  • thenable 会被吸收并按 Promise 规则执行

这也是符合原生 Promise.race 的行为。

5. 更完整的版本:处理空 iterable

原生 Promise.race([]) 会返回一个永远 pending 的 Promise。

你现在的实现遇到空数组也会返回 pending(因为 for 循环不执行),这符合原生。

如果你想更明确一点,可以加个计数,但不是必须。

6. 更多测试用例

6.1 测试(先 reject)

1
2
3
4
5
6
7
const p1 = new Promise(res => setTimeout(() => res('p1'), 1000));
const p2 = new Promise(res => setTimeout(() => res('p2'), 500));
const p3 = new Promise((res, rej) => setTimeout(() => rej('p3 error'), 300));

myPromiseRace([p1, p2, p3])
.then(console.log)
.catch(console.error); // 输出: p3 error

6.2 普通值参与竞速(会立刻赢)

1
2
3
4
myPromiseRace([Promise.resolve('A'), 123, new Promise(res => setTimeout(() => res('B'), 10))])
.then(console.log)
.catch(console.error);
// 输出:123(普通值最快)

6.3 空数组:永远 pending

1
2
3
const p = myPromiseRace([]);
setTimeout(() => console.log('still pending...'), 1000);
// 1 秒后输出 still pending...,race 本身没有结果

7. 实战:用 race 做请求超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
});
}

async function fetchWithTimeout(url, ms = 3000) {
return myPromiseRace([fetch(url), timeout(ms)]);
}

// 用法
fetchWithTimeout('https://example.com/api', 2000)
.then(res => res.json())
.then(data => console.log('data:', data))
.catch(err => console.error('error:', err.message));

8. 面试常问点(加分项)

  • Q:race 能取消其他 Promise 吗?
    不能。Promise 本身不支持取消,除非你用 AbortController(fetch)或自己设计可取消任务。

  • Q:race 里放普通值会怎样?
    会被 Promise.resolve 包装成 fulfilled Promise,通常会立刻“获胜”。

  • Q:race 和 any/allSettled 有什么区别?

    • race:谁先 settle 用谁
    • any:谁先 fulfilled 用谁(全部 rejected 才 rejected)
    • allSettled:等全部结束,返回每个结果状态

9. 总结

手写 Promise.race 的核心只有一句话:

遍历 iterable,把每个元素 Promise.resolve 后接上同一个 resolve/reject,谁先 settle 就定局。

-------------本文结束感谢您的阅读-------------