一个页面同时加载用户资料、通知和推荐内容。如果推荐接口失败,资料和通知是否还要展示?如果三个备用地址里有一个很快报错,页面是立即失败,还是等另一个地址成功?
Promise 的四种聚合方法,区别就体现在这些结束条件上。先说清楚页面允许什么结果,再选方法,通常比从方法名字出发更容易写对。
先比较结束条件和结果形状
| 方法 | 聚合结果什么时候成功 | 什么时候失败 | 返回什么 |
|---|---|---|---|
| Promise.all | 所有输入都成功 | 任意输入失败 | 按输入顺序排列的值 |
| Promise.allSettled | 所有输入都已成功或失败 | 普通输入 Promise 的失败会记录在结果中 | 按输入顺序排列的状态对象 |
| Promise.race | 最先结束的输入成功 | 最先结束的输入失败 | 该输入的值或拒绝原因 |
| Promise.any | 任意输入成功 | 所有输入都失败 | 首个成功值,或 AggregateError |
这里的 allSettled 不能概括成任何情况下都不会失败。输入的迭代过程等也可能抛错。它适合收集普通任务各自的成功和失败状态。MDN Promise 文档列出了这些并发方法的规则。
all 和 allSettled 的结果顺序取决于输入顺序。如果第 2 个请求先完成,它的结果仍在数组第 2 项。完成顺序和结果排列顺序需要分开看。
必须全部拿到,还是允许局部失败
假设进入某个页面必须同时拿到权限和配置,两者缺一不可,all 可以表达这个要求。某个任务失败后,聚合 Promise 就会拒绝,调用方进入统一错误处理。
如果资料必须成功,但推荐内容可以稍后补上,就不宜把两者简单当成同样的必需任务。可以分别处理,或者用 allSettled 收集结果,再根据业务规则决定显示哪些部分。
allSettled 的输出包含 fulfilled 或 rejected 状态。失败项保留拒绝原因,成功项保留值。这样可以展示部分内容、记录失败项,或只重试其中一些任务。
javascript
async function collectResults() {
const jobs = [
Promise.resolve("profile"),
Promise.reject(new Error("recommendation unavailable")),
];
const results = await Promise.allSettled(jobs);
console.log(results.map(item => item.status));
}
collectResults().catch(console.error);
// ["fulfilled", "rejected"]
这段代码用已经成功或失败的 Promise 演示结果结构,没有模拟真实接口。页面是否能接受局部失败,仍然要由产品行为决定。
race 要最快结束,any 要第一个成功
下面用两个定时任务模拟备用服务。A 很快失败,B 稍后成功,延迟只用于演示先后顺序。
javascript
function makeJobs() {
return [
new Promise((_, reject) => {
setTimeout(() => reject(new Error("A unavailable")), 10);
}),
new Promise(resolve => {
setTimeout(() => resolve("B ready"), 30);
}),
];
}
async function compare() {
try {
console.log("race", await Promise.race(makeJobs()));
} catch (error) {
console.log("race", error.message);
}
console.log("any", await Promise.any(makeJobs()));
}
compare().catch(console.error);
// race A unavailable
// any B ready
race 很快得到失败结果,any 则继续等待可用结果。如果业务需求是从备用服务里拿到一个成功响应,这两种方法不能互换。
示例使用 JavaScript 原生 Promise API。Python 的 asyncio 有自己的任务调度和取消规则,不能把这四个方法机械对应成四段 Python 代码。
聚合结果结束,不会自动取消其他任务
all 提前失败以后,其余请求可能继续运行。race 已经返回一个结果,其他输入也可能还在等待。any 拿到成功值,同样不会自动终止剩余任务。
若底层 API 支持取消,可以结合 AbortController 等机制主动取消不再需要的请求。涉及写操作时,还要考虑服务端已经执行了多少。客户端停止等待,并不能证明服务端没有创建订单或修改数据。
超时包装也有同样的问题。把网络请求和一个拒绝的定时任务放进 race,可以限制调用方等多久,却没有自动限制服务端执行多久。需要释放资源时,超时处理还应触发底层取消,并清理定时器。
fetch 的成功状态与业务成功还要再判断
fetch 收到 404、500 等 HTTP 响应时,通常会得到一个 fulfilled 的 Response。聚合方法看到的是 Promise 状态,不会替你把 HTTP 错误判断成 rejected。
如果要让这些响应进入失败分支,需要在请求函数里检查 response.ok,按业务要求抛错,再把这个请求函数返回的 Promise 交给聚合方法。MDN Fetch 使用指南解释了这一步。
空输入和并发限制别遗漏
空输入时,all 和 allSettled 成功返回空数组,any 以 AggregateError 拒绝,race 会一直保持 pending。封装通用批处理函数时,应明确空集合的行为。
还有一个常见误区。调用请求函数时,工作可能已经开始;all 负责聚合状态,不负责把一万个请求限成五路并发,也不会自动把主线程重计算分配给多个 CPU。任务数量很多时,需要另外设计执行队列或并发上限。
我是程序员Sunday,也在整理「sunday面试指南」。对应的完整问答是 Promise 四种并发方法与取消行为,里面补充了常见面试追问。