先记住这个答案
Promise.allSettled 等所有任务都落定后,返回一个和输入等长的数组,每一项是一个描述结果的对象。成功的项长这样:{ status: 'fulfilled', value: 兑现值 };失败的项长这样:{ status: 'rejected', reason: 拒绝原因 }。一项只会有 value 或 reason 其中一个字段,顺序和传入顺序一致。它返回的 Promise 自己永远不会 reject。
- 成功项是
status加value两个字段 - 失败项是
status加reason两个字段 - 结果顺序等于传入顺序,与完成先后无关
- 判断成败只看
status,别看value有没有值
把四种输入的结果打印出来
成功的 Promise、失败的 Promise、普通值,再加一个不带原因的拒绝,看看每一项变成什么样。
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error('超时')),
'plain', // 普通值也会被当成成功
Promise.reject() // 拒绝原因是 undefined
]).then((results) => {
results.forEach((r) => console.log(Object.keys(r).join(' + '), '->', r.status))
console.log(results[0].value, results[1].reason.message, results[2].value)
console.log('reason' in results[3], results[3].reason)
})查看输出与解释
status + value -> fulfilled
status + reason -> rejected
status + value -> fulfilled
status + reason -> rejected
1 超时 plain
true undefined每一项都只有两个字段,没有第三种形状。普通值 'plain' 会先被 Promise.resolve 包一层,所以也算成功。
最后一项值得多看一眼:Promise.reject() 没给原因,结果对象里 reason 字段依然存在,只是值为 undefined。同理,一个兑现值是 undefined 的成功项,value 也是 undefined。所以用 r.value 有没有值来判断成败是靠不住的,唯一可靠的是 status。
拿到结果后怎么用
最常见的用法是把成功和失败拆开:成功的拿去渲染,失败的拿去上报或提示。
const results = await Promise.allSettled(ids.map((id) => getUser(id)))
// 类型守卫:告诉 TS 过滤后剩下的都是成功项
const users = results
.filter((r): r is PromiseFulfilledResult<User> => r.status === 'fulfilled')
.map((r) => r.value) // 这里 r.value 的类型是 User
const errors = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map((r) => r.reason) // reason 的类型是 any,用之前要自己判断TypeScript 内置了这两种结果的类型:PromiseFulfilledResult<T> 和 PromiseRejectedResult,两者合起来就是 PromiseSettledResult<T>。直接写 .filter((r) => r.status === 'fulfilled'),老版本的 TS 推断不出过滤后的类型,后面访问 value 会报错,所以要写成 r is ... 这种类型守卫。
reason 的类型是 any,因为 JavaScript 里什么都能被 reject,不一定是 Error 对象。取 message 之前最好先判断一下 reason instanceof Error。
- 需要按位置对应时别过滤
- 如果三个结果分别对应页面上三个区块,就不要
filter,直接解构:const [user, orders, recs] = results,再分别看各自的status。过滤之后下标就变了,没法知道哪一项对应哪个接口。
容易答错的地方
- 以为失败项也有
value字段 - 失败项只有
status和reason,没有value。反过来,成功项也没有reason。有人会写r.value || r.reason来取值,这样在兑现值是0、空字符串时就会误取到undefined。 - 在
Promise.allSettled后面写catch处理失败 - 它返回的 Promise 总是兑现,
catch里永远收不到某一项的失败。失败信息必须从结果数组里按status找出来。 - 把
status记成resolved status的取值是fulfilled和rejected,不是resolved。resolved 在规范里的意思是「命运已经被锁定」,一个 Promise 被另一个还在 pending 的 Promise 决议时也叫 resolved,但它的状态还没定,所以不能拿来当状态名。
面试官还会怎么问?
手写 Promise.allSettled 怎么写?
对每一项调用 Promise.resolve(item).then(value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason })),把失败也转成兑现,再把这些 Promise 交给 Promise.all。因为转换后没有任何一项会 reject,Promise.all 就会等到全部结束。
和 Promise.all 的结果有什么不同?
Promise.all 成功时结果数组里直接是兑现值,比如 [1, 2]。换成 Promise.allSettled,每一项都是描述对象,要多取一层 value。另外 Promise.all 遇到失败就整体拒绝,Promise.allSettled 永远兑现。
传入空数组会得到什么?
和 Promise.all([]) 一样,返回一个已经兑现、值为空数组的 Promise。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。