先记住这个答案
Promise.race([]) 会返回一个永远处于 pending 的 Promise。race 的规则是谁先完成就跟谁,它内部会给数组里每个元素挂上 then,第一个回调触发时决定结果。空数组里没有元素,就没有任何回调会触发,返回的 Promise 也就永远不会成功或失败。这和 Promise.all([]) 立刻以 [] 成功、Promise.any([]) 立刻失败都不一样,面试时常把这几个放在一起问。
Promise.race([])永远 pending,不报错也不结束Promise.all([])立刻以空数组成功Promise.any([])立刻以AggregateError失败- 数组可能为空时,调用
race前先判断长度
四个组合方法在空数组下的表现
同样传空数组,四个方法给出了三种结果。放在一起跑一遍最直观。
Promise.all([]).then((v) => console.log('all:', JSON.stringify(v)))
Promise.allSettled([]).then((v) => console.log('allSettled:', JSON.stringify(v)))
Promise.any([]).catch((e) => console.log('any:', e.name, e.errors.length))
Promise.race([]).then(
() => console.log('race 成功'), // 永远不会执行
() => console.log('race 失败') // 也永远不会执行
)
setTimeout(() => console.log('1 秒后 race 仍然没有结果'), 1000)查看输出与解释
all: []
allSettled: []
any: AggregateError 0
1 秒后 race 仍然没有结果race 的两个回调都没打印,不管等多久都一样。
这三种结果背后是同一个道理:看「等到什么时候算结束」。all 和 allSettled 要等全部完成,零个元素可以认为已经全部完成,所以立刻成功,结果是空数组。any 要等第一个成功,如果全部都失败了就报错,零个元素可以认为已经全部失败,所以立刻以 AggregateError(一种把多个错误打包在一起的错误类型)失败,errors 是空数组。race 要等第一个完成,没有元素就永远等不到第一个,而它又没有「全部结束」这个兜底条件,只能一直 pending。
业务代码里怎么踩到、怎么防
没人会手写 Promise.race([]),出问题的都是数组来自动态数据的时候。比如从多个 CDN 节点里选响应最快的一个,节点列表是接口下发的;某次配置出错返回了空列表,await Promise.race(list) 就卡住了,页面停在 loading,没有报错,也没有日志。
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms))
}
function fastest(tasks) {
if (tasks.length === 0) return Promise.reject(new Error('没有可用的节点')) // 空列表明确失败
return Promise.race(tasks)
}
fastest([]).catch((e) => console.log('失败:', e.message))
fastest([delay(300, 'A'), delay(100, 'B')]).then((v) => console.log('最快:', v))查看输出与解释
失败: 没有可用的节点
最快: B空列表时是返回失败还是返回一个默认值,要看业务:选节点这种没有就不能继续的,应该失败;可有可无的,可以直接 Promise.resolve(默认值)。关键是不能让它悄悄 pending。
- 超时兜底
- 另一种做法是把超时也放进
race里,比如Promise.race([...tasks, timeout(5000)]),这样数组至少有一个元素,最坏 5 秒后也会以超时失败。这种写法一般本来就要加,用来防某个请求一直不回来,顺带也防住了空数组。
容易答错的地方
- 以为空数组会立刻成功或者报错
- 很多人类比
Promise.all([]),以为race也会立刻以undefined成功,或者以为它会抛TypeError。都不对,它不成功、不失败、不报错。只有传入的不是可迭代对象,比如Promise.race(null),才会以TypeError拒绝。 - 以为 pending 的 Promise 会超时或被回收时报错
- Promise 本身没有超时机制,pending 可以一直持续到页面关闭。它也不会触发
unhandledrejection事件,因为根本没有拒绝。如果不再有引用指向它,它会被垃圾回收,挂在上面的then回调和await之后的代码就再也没有机会执行了。
面试官还会怎么问?
手写 Promise.race 时,空数组要特殊处理吗?
不用。标准写法是返回 new Promise((resolve, reject) => { for (const p of list) Promise.resolve(p).then(resolve, reject) }),数组为空时循环一次都不执行,resolve 和 reject 都不会被调用,自然就是永远 pending,和规范行为一致。
race 的数组里有一个普通值,比如 Promise.race([1, delay(100)]),结果是什么?
结果是 1。普通值会先被 Promise.resolve 包成一个已经成功的 Promise,它的 then 回调在下一个微任务就触发,比任何定时器都快。
race 选出结果之后,其他 Promise 会被取消吗?
不会。race 只是不再关心其他结果,那些请求和定时器照样执行完。真要取消,需要配合 AbortController,拿到结果后调用 abort()。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。