如何手写 Promise.race、allSettled 和 any?
简化版
race 谁先落定就跟谁走;allSettled 等所有项结束并返回状态明细;any 谁先成功就成功,全部失败才失败。手写时要处理普通值、空输入、结果顺序和失败收集,不能把三者都写成 Promise.all 的变体。
详细版
三者都可以先把 iterable 转成数组,再用 Promise.resolve 统一包装每一项。race 对每一项同时注册 resolve/reject;allSettled 无论成功失败都写入 { 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 成功,最终仍然成功,前两个失败只作为过程中间状态。
五、空输入和普通值边界
| 输入 | race | allSettled | any |
|---|---|---|---|
[] | pending | fulfilled [] | 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。 - 误区:
any和race一样。any忽略单项失败,直到全部失败才拒绝。 - 追问:为什么
any([])要失败? 没有任何候选能成功,所以结果是聚合失败。 - 追问:
AggregateError里应该放什么? 放每个失败原因,通常按输入顺序保存。 - 追问:超时竞速会取消慢请求吗? 不会,取消需要底层 API 支持。
八、加强记忆
把三者记成三个裁判:race 是最急的裁判,谁先有结果就吹哨;allSettled 是登记员,等所有人交卷再给状态表;any 是容错裁判,只要有人成功就放行,所有人都失败才判失败。手写时围绕短路条件、空输入和结果顺序展开,基本不会偏。