先记住这个答案
Promise.all 不要求每一项都是 Promise。它会对每一项调用 Promise.resolve:原生 Promise 原样返回,普通值被包成一个已兑现的 Promise,带 then 方法的对象(thenable,长得像 Promise 的对象)会被当成 Promise 去等。所以普通值在结果数组里原样出现,位置和传入时一样。整体兑现依然是异步的,哪怕数组里全是普通值,也要在 then 或 await 里才能拿到结果。
- 每一项都会过一遍
Promise.resolve - 普通值原样出现在结果里,位置不变
- 有
then方法的对象会被当成 Promise 等待 - 全是普通值时,结果仍是异步拿到的
普通值、Promise、thenable 混在一起
实际代码里经常出现这种情况:一部分数据有缓存,直接是值;另一部分要请求,是 Promise。放进同一个 Promise.all 就行,不用先判断类型。
const thenable = { then(resolve) { resolve('thenable 的值') } }
Promise.all([1, 'a', null, Promise.resolve(2), thenable]).then((res) => {
console.log(JSON.stringify(res))
})
console.log('同步代码结束')查看输出与解释
同步代码结束
[1,"a",null,2,"thenable 的值"]数字、字符串、null 都原样保留了。Promise.resolve(2) 取出了兑现值 2。至于 thenable 这个对象,它本身没有出现在结果里,出现的是它通过 resolve 交出来的字符串,因为它有 then 方法,被当成 Promise 处理了。「同步代码结束」先打印,说明就算大部分是普通值,结果也不是同步给出的。
这就是为什么缓存读取的函数不用非得写成 async:返回值是普通值还是 Promise,Promise.all 都能吃下。
结果顺序为什么不受完成先后影响
Promise.all 遍历时给每一项记住它的下标,某一项兑现后,把值写进结果数组对应的位置,而不是 push 到末尾。所以结果顺序永远等于传入顺序,和谁先完成无关。
const delay = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
// 慢的放前面,普通值放中间,快的放后面
Promise.all([delay('慢', 30), '普通值', delay('快', 10)]).then((res) => {
console.log(res.join(' | '))
})
Promise.all(123).catch((err) => console.log(err.name))查看输出与解释
TypeError
慢 | 普通值 | 快「快」10 毫秒就完成了,普通值更早,但结果数组里它们仍然待在自己的位置上。这让你可以放心用解构取值:const [user, config, list] = await Promise.all([...]),不用担心对不上号。
最后一行传了一个数字 123,而 Promise.all 要的是可迭代对象,不是「数组里的每一项」必须是什么。数字不可迭代,所以返回的 Promise 以 TypeError 拒绝。注意它是拒绝,不是同步抛错,外面包 try 接不住,要用 catch。
容易答错的地方
- 以为普通值会让
Promise.all报错或被跳过 - 普通值既不会报错,也不会被过滤。结果数组的长度永远等于输入的长度,普通值在哪一位,结果就在哪一位。
- 以为全是普通值时可以同步拿到结果
Promise.all([1, 2, 3])返回的仍然是一个 Promise,每一项都要经过一次then才会写进结果,这些then回调在微任务里执行。所以console.log(Promise.all([1, 2]))打印的是 Promise 对象,不是[1, 2]。- 忘了普通对象上可能有
then - 如果你放进去的数据对象恰好有一个
then方法,比如某些链式调用的查询构造器,Promise.all会调用它的then并等待,拿到的不是这个对象本身。如果它的then永远不调用回调,整个Promise.all就会一直 pending。
面试官还会怎么问?
结果里的对象和传进去的是同一个引用吗?
是。普通对象被 Promise.resolve 包装时不会被复制,兑现值就是原对象的引用。在结果里修改它,原来那个变量看到的也会变。
手写 Promise.all 时怎么处理普通值?
不要自己判断 instanceof Promise,直接对每一项调用 Promise.resolve(item).then(...)。这样普通值、原生 Promise、thenable 三种情况一次全覆盖,也和规范行为一致。
数组里有一个 undefined 会怎样?
和其他普通值一样,被包成兑现值为 undefined 的 Promise,结果数组对应位置就是 undefined。它不代表失败,想表示失败要让那一项 reject。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。