
JavaScript 里 Promise 有四个用于并发调度的静态方法——Promise.all、Promise.race、Promise.any和Promise.allSettled。
很多人搞不清楚这几个方法的区别,今天我们就来简单梳理一下。
在正式开始之前,我们先来想一个真实场景。
一顿年夜饭
假设除夕夜你要张罗一桌年夜饭,同时准备三道菜:
- 炖排骨(砂锅慢炖,要 60 分钟)
- 蒸鲈鱼(上锅蒸,要 15 分钟)
- 拍黄瓜(切菜拌蒜,要 5 分钟)
这三件事可以同时进行——这就是异步。你不会傻到先炖完排骨再蒸鱼再拍黄瓜,那样等开始吃年夜饭的时候已经是半夜了。
每道菜的准备过程就是一个 Promise:它最终会完成(fulfilled),也可能搞砸(rejected)——排骨炖干了,鱼蒸老了,黄瓜拌咸了。
现在问题是:你到底怎么等这三道菜?
这取决于你是什么样的厨师。而这四种等待方式,正好对应四个 Promise 方法。
第一种厨师:完美主义者 → Promise.all
这位厨师说:
"三道菜全部成功,我才能上桌。任何一道搞砸了,今晚的年夜饭就散了。"
这就是 Promise.all。
const dinner = Promise.all([
stewRibs(), // 炖排骨
steamFish(), // 蒸鲈鱼
tossCucumber() // 拍黄瓜
]);
dinner
.then(([ribs, fish, cucumber]) => {
console.log("开饭了!三道菜全部就位。");
})
.catch(error => {
console.log("年夜饭散了——" + error);
});
它的规则很简单:
- 三道菜全部成功 → 你拿到一个数组
[排骨, 鲈鱼, 黄瓜],顺序和你传进去的顺序一模一样。注意,是你传进去的顺序,不是完成的顺序——拍黄瓜 5 分钟就好了,排骨 60 分钟才好,但结果数组里排骨排第一。 -
任何一道菜失败 → 整个
Promise.all立刻 reject,带上那个失败的原因。其他菜还在做?它不管了。
这里有个关键细节很多人没想清楚:"立刻 reject" 不等于 "取消其他任务"。
Promise 一旦开始就没有"取消"这回事。排骨还在砂锅里炖着呢,只是厨师不再等它了。如果排骨最终炖干了,那个 rejection 会被静默吞掉(在 Node.js 里可能触发 unhandledRejection 警告)。这就像你宣布年夜饭散了,但灶上的火还开着——你得自己记得去关。
一句话总结 Promise.all:全部成功才算成功,一个失败就全盘否定。
第二种厨师:急性子 → Promise.race
这位厨师说:
"我等不了那么多。谁先完成——不管是做好了还是搞砸了——我就认那个结果。"
这就是 Promise.race。
想象一场真正的赛跑。跑道上三个人同时出发。第一个冲过终点线的人,赢了。但如果第一个人在中途摔倒了(reject),那他也是"第一个完成的"——比赛结束,结果是"输了"。
const result = Promise.race([
fetchFromCache(), // 从缓存读,快
fetchFromNetwork(), // 从网络读,慢
timeout(5000) // 5秒超时
]);
result
.then(value => {
console.log("第一个回来的赢了:" + value);
})
.catch(error => {
console.log("第一个回来的搞砸了:" + error);
});
它的规则更简单:
- 第一个 promise 完成(fulfilled)→
Promise.race就 fulfilled,带上那个值。 - 第一个 promise 失败(rejected)→
Promise.race就 rejected,带上那个原因。 - 不管是成功还是失败,第一个定局的(settled)说了算,其他的全部忽略。
最常见的用法就是上面这种——竞速。缓存和网络同时请求,谁先回来用谁。再加一个超时 promise,如果 5 秒内谁都没回来,超时 promise 先 reject,整个 race 就失败了。
这里也有一个容易忽略的点:race 不是"第一个成功的"。是"第一个有结果的"。如果第一个有结果是失败,那就是失败。如果你要的是"第一个成功的",那不是 race,那是另一个故事了——你得自己写。
一句话总结 Promise.race:第一个定局的定胜负,不论成败。
第三种厨师:乐观主义者 → Promise.any
这位厨师说:
"只要有一道菜做好了,我就能上桌。全搞砸了,那才是真的完了。"
这就是 Promise.any。
你在 race 那里看到了——第一个有结果的说了算,不管成败。但有时候你要的不是"第一个有结果的",你要的是"第一个成功的"。比如你同时向三个服务器发请求,哪个先返回有效数据就用哪个。一个服务器报错了?无所谓,等下一个。只有三个全挂了你才认输。
const data = Promise.any([
fetchFromServer1(), // 主服务器
fetchFromServer2(), // 备用服务器
fetchFromServer3() // 备用服务器
]);
data
.then(value => {
console.log("第一个成功的数据:" + value);
})
.catch(errors => {
console.log("全部失败——", errors); // AggregateError
});
它的规则:
- 第一个 promise 成功(fulfilled)→
Promise.any就 fulfilled,带上那个值。其他的不等了。 - 只有全部失败 →
Promise.any才 reject,而且 reject 的不是一个普通错误,是一个AggregateError——把所有失败原因打包在一起的错误。 - 某些先失败了?不管。继续等。 直到有一个成功,或者全部失败。
注意它和 race 的区别——这是最容易搞混的一对:
-
race:第一个有结果的(成功或失败)说了算。 -
any:第一个成功的说了算。失败的不算数,继续等。
一个微妙但关键的细节:any 会忽略 rejection直到有 fulfilled 出现。这意味着如果有三个 promise,第一个立刻 reject 了,any 不会立刻 reject——它会继续等另外两个。只有在所有 promise 都 reject 之后,它才打包成一个 AggregateError 抛出来。
这是 ES2021 才加入的方法。你可能觉得"这不就是把 race 加个成功过滤吗?"——对,从行为上看确实如此。但从语义上看,它解决的是一个完全不同的问题:容错。race 是竞速,any 是冗余备份。
一句话总结 Promise.any:第一个成功的赢,全部失败才算输。
第四种厨师:记录员 → Promise.allSettled
这位厨师说:
"三道菜我都要等。做好的记下来做好了,搞砸的记下来搞砸了。我要的是全部的记录,不是一个最终结论。"
这就是 Promise.allSettled。
想象你在做一份问卷调查。你发了 100 份问卷出去。有人认真填了,有人拒答了,有人填了一半放弃了。但你作为调查员,你要的不是"只要所有人都认真填了我才看"——你要的是所有人的反馈,不管是什么反馈。
const report = Promise.allSettled([
stewRibs(),
steamFish(),
tossCucumber()
]);
report.then(results => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`第 ${index + 1} 道菜做好了:${result.value}`);
} else {
console.log(`第 ${index + 1} 道菜搞砸了:${result.reason}`);
}
});
});
它的规则:
- 等所有 promise 全部 settle(不管是 fulfilled 还是 rejected)。
- 永远不会 reject。永远不会。
- 返回一个数组,每个元素是
{ status, value }或{ status, reason }:- 成功的:
{ status: "fulfilled", value: 结果 } - 失败的:
{ status: "rejected", reason: 失败原因 }
- 成功的:
这是 2020 年才加入标准的方法(ES2020),比另外两个晚了很多年。为什么?因为人们在实践中发现,很多时候你不想要"一个失败就全盘放弃"的行为。你群发 100 条消息,3 条发送失败了,你不想整批都报错——你想知道哪 3 条失败了,然后重发。这就是 allSettled 存在的意义。
一句话总结 Promise.allSettled:全部等完,成败都记,永不报错。
把四个放在一起看
好,现在我们有了物理图像。让我把四个厨师请到同一张八仙桌前:
| Promise.all | Promise.race | Promise.any | Promise.allSettled | |
|---|---|---|---|---|
| 等什么 | 全部完成 | 第一个完成 | 第一个成功 | 全部完成 |
| 成功条件 | 全部成功 | 第一个成功的成功(但第一个失败的也算"完成") | 第一个成功的 | 永远成功 |
| 失败条件 | 任何一个失败 | 第一个失败的失败 | 全部失败 | 永远不失败 |
| 返回什么 | 结果数组 | 第一个定局的结果 | 第一个成功的结果 | 状态记录数组 |
| 会不会 reject | 会 | 会 | 会(全部失败时) | 不会 |
| reject 带什么 | 第一个失败原因 | 第一个失败原因 | AggregateError(全部原因) | — |
| 什么时候用 | 必须全部成功才有意义 | 只要最快的一个 | 容错备份,只要一个成功 | 每个结果都要知道 |
你看,这四个方法的区别不在于"语法",而在于你对失败的容忍度。
-
all:零容忍。一个失败,全部作废。 -
race:无所谓成败,只要最快。 -
any:失败可以忽略,只要有一个成功。 -
allSettled:全容忍。失败也要,我全都要。
一个让你真正理解的问题
我现在问你一个问题。你不用回答我,你自己想想。
如果
Promise.all里的某一个 promise 失败了,其他 promise 还在跑吗?
如果你犹豫了,说明你还在"知道名字"的阶段。
答案是:还在跑。 Promise 没有取消机制。Promise.all 只是不再等它们了,但它们还在后台执行。排骨还在砂锅里炖着呢。
这个问题为什么重要?因为它揭示了 Promise 的一个本质特性——Promise 是不可取消的。 一旦创建,它就会跑到 settle 为止。Promise.all 的"快速失败"只是在"等待"层面失败了,不是在"执行"层面失败了。这两个层面的区别,恰恰是很多人 bug 的来源。
如果你真想要"一个失败就取消其他的"行为,你需要用 AbortController——但那是另一回事了,今天不展开。
最后一件事:别只记结论
很多人看完这种文章会说"哦我懂了,all 是全部成功,race 是第一个,any 是第一个成功的,allSettled 是全部等完"。
不,你只是记住了结论。
真正理解的方式是:你能不能自己重新造出这四个方法?
让我给你看 Promise.all 的本质——它不过就是这么个东西:
function myPromiseAll(promises) {
return new Promise((resolve, reject) => {
const results = new Array(promises.length);
let completed = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(value => {
results[index] = value; // 按原始位置存放
completed++;
if (completed === promises.length) {
resolve(results); // 全部成功才 resolve
}
}, reject); // 任何一个失败,直接 reject
});
});
}
看到没有?核心逻辑就两条:
- 用一个计数器,每完成一个加一,等到全部完成。
- 任何一个失败,直接调
reject,后面的不管了。
Promise.race 更简单——甚至没有计数器:
function myPromiseRace(promises) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
Promise.resolve(promise).then(resolve, reject);
// 第一个调用 resolve 或 reject 的,就定了
// 后面的调用会被 Promise 忽略——Promise 只 settle 一次
});
});
}
它的精妙之处在于利用了 Promise 的一个基本特性:一个 Promise 只能被 settle 一次。 第一个调用 resolve 或 reject 的人赢了,后面的调用自动失效。你不需要写任何"判断是否已经完成"的逻辑——Promise 帮你做了。
Promise.any 则是 race 和 all 的混合体——成功的行为像 race,失败的行为像 all 的反面:
function myPromiseAny(promises) {
return new Promise((resolve, reject) => {
const errors = new Array(promises.length);
let rejectedCount = 0;
if (promises.length === 0) {
reject(new AggregateError([], "All promises were rejected"));
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
resolve, // 第一个成功的,直接 resolve——和 race 一样
reason => {
errors[index] = reason; // 记录失败原因
rejectedCount++;
if (rejectedCount === promises.length) {
// 全部失败了,打包成 AggregateError
reject(new AggregateError(errors, "All promises were rejected"));
}
}
);
});
});
}
看到没有?any 的成功分支直接调 resolve——跟 race 一模一样,第一个成功就赢。但失败分支不一样:它不立刻 reject,而是计数。只有所有 promise 都失败了,它才把错误打包成一个 AggregateError 抛出去。这正是"第一个成功的赢,全部失败才算输"的字面翻译。
Promise.allSettled 则是把两个都等完:
function myPromiseAllSettled(promises) {
return new Promise(resolve => { // 注意:只有 resolve,没有 reject
const results = new Array(promises.length);
let completed = 0;
if (promises.length === 0) {
resolve(results);
return;
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => {
results[index] = { status: "fulfilled", value };
completed++;
if (completed === promises.length) resolve(results);
},
reason => {
results[index] = { status: "rejected", reason };
completed++;
if (completed === promises.length) resolve(results);
}
);
});
});
}
看到区别了吗?allSettled 的成功和失败分支都做同一件事——记录结果,计数器加一,检查是否全部完成。它永远走 resolve,因为对它来说,失败也是一种"结果",不是一种"错误"。
所以,到底选哪个?
不要问"我应该用哪个"。问自己一个问题:
当其中一个任务失败时,我在乎吗?
- "我在乎,一个失败全盘皆输" →
Promise.all - "我不在乎成败,我只要最快的" →
Promise.race - "我不在乎失败,只要有一个成功就行" →
Promise.any - "我不在乎失败,但我要知道每个的结果" →
Promise.allSettled
这就是全部了。