先记住这个答案
Promise.all([]) 会返回一个已经 fulfilled 的 Promise,兑现值是空数组 [],不会一直 pending,也不会报错。道理很简单:Promise.all 的语义是「所有任务都成功就成功」,一个任务都没有,自然算全部成功。要注意的是,状态虽然当场就定下来了,但 then 里的回调依然是异步执行的,同步代码先跑完才轮到它。
Promise.all([])立刻 fulfilled,值是[]- 状态是同步定下的,
then回调仍在微任务里执行 Promise.race([])才会永远 pendingPromise.any([])直接以AggregateError拒绝
四个组合方法遇到空数组
面试官问完 Promise.all,经常顺手问另外三个。放在一起看最好记。
Promise.all([]).then((res) => console.log('all', JSON.stringify(res)))
Promise.race([]).then(() => console.log('race 永远不会打印'))
Promise.any([]).catch((err) => console.log('any', err.name))
Promise.allSettled([]).then((res) => console.log('allSettled', JSON.stringify(res)))
console.log('同步代码结束')查看输出与解释
同步代码结束
all []
any AggregateError
allSettled []先打印「同步代码结束」,说明回调都是异步的。Promise.all 和 Promise.allSettled 都拿到空数组。Promise.any 的语义是「有一个成功就成功」,一个任务都没有,就不可能有人成功,所以直接以 AggregateError 拒绝。Promise.race 是「谁先落定听谁的」,没有参赛选手,比赛永远不会结束,返回的 Promise 就一直 pending,then 永远不会执行。
记的时候可以这样想:「全部」类的 Promise.all 和 Promise.allSettled 对空集合直接成功,「至少一个」类的 Promise.any 对空集合直接失败,而 Promise.race 压根没有答案。
为什么说状态是同步定下的
Promise.all 内部维护一个计数器,记录还有几个任务没完成。每有一个任务兑现,计数就减一,减到 0 时用结果数组兑现返回的 Promise。空数组遍历完,计数器一开始就是 0,所以在 Promise.all([]) 这一行执行的当下,返回的 Promise 就已经是 fulfilled 了。
[1] 对比一下JavaScript// 和一个已兑现的普通值赛跑,谁先赢说明谁先落定
const probe = (p) => Promise.race([p, '还在 pending'])
probe(Promise.all([])).then((v) => console.log('空数组:', JSON.stringify(v)))
probe(Promise.all([1])).then((v) => console.log('[1]:', JSON.stringify(v)))查看输出与解释
空数组: []
[1]: "还在 pending"空数组那个 Promise 在赛跑开始前就已经兑现,排在「还在 pending」前面赢了。Promise.all([1]) 不一样,它要先把 1 包成 Promise.resolve(1),再通过 then 等它兑现,这一步要走一次微任务,所以赛跑时它还没落定,输给了普通值。
这个区别平时写业务基本碰不到,因为你拿结果只能通过 then 或 await,两种情况都是异步拿到的。面试时说清「状态同步确定,回调异步执行」这两层就够了。
容易答错的地方
- 以为空数组会让
Promise.all一直等下去 - 会一直等的是
Promise.race([]),不是Promise.all([])。把两者搞混,是这道题最常见的错。 - 以为状态是 fulfilled 就能同步拿到值
- Promise 没有同步读取结果的 API,就算它已经兑现,
then回调和await后面的代码也要等当前同步代码执行完才运行。const res = Promise.all([])拿到的是 Promise 对象,不是[]。 - 把空结果当成接口出错
- 比如根据勾选的 id 批量删除,用户一个都没勾,
Promise.all([])立刻成功,界面提示「删除成功」。这不是Promise.all的问题,是调用前没判断列表是否为空。
面试官还会怎么问?
传的不是数组,而是 undefined 会怎样?
Promise.all 要求参数可迭代。传 undefined、数字这类不可迭代的值,它不会同步抛错,而是返回一个以 TypeError 拒绝的 Promise。所以要在 catch 里处理,而不是在外面包 try。
传一个字符串 'abc' 呢?
字符串是可迭代的,会被拆成三个单字符的普通值,结果是 ['a', 'b', 'c']。这属于合法但没什么用的写法,面试里能说出「只要可迭代就行,不限于数组」就可以。
自己手写 Promise.all 时,空数组要怎么处理?
手写版常见的写法是在每个任务兑现时计数,等计数等于长度再 resolve。空数组根本不会进入循环,计数永远等不到,返回的 Promise 就一直 pending。所以要在循环前加一句:长度为 0 时直接 resolve([])。这是手写题里很容易被扣分的边界。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。