在实际开发中,经常需要同时发起多个异步操作(比如并发请求多个 API、读取多个文件),并根据它们的结果执行后续逻辑。Node.js 完全支持 ECMAScript 的 Promise 静态方法,这些工具可以帮助我们用简洁的代码控制并发、收集结果、处理异常,无需手动编写计数器或状态跟踪。本节逐个介绍 Promise.all、Promise.allSettled、Promise.race 和 Promise.any,着重于它们的差异和真实使用场景。
11.2.1 Promise.all:并发执行,全部成功才返回
语法:Promise.all(iterable)
参数:一个可迭代对象(通常是数组),其中每个元素是一个 Promise。
返回值:一个新 Promise。
- 当 所有 传入的 Promise 都变为
fulfilled时,返回的 Promise 才会变为fulfilled,结果是一个数组,包含每个输入 Promise 的返回值,顺序与输入顺序一致。 - 只要 任意一个 传入的 Promise 变为
rejected,返回的 Promise 会立即变为rejected,拒绝原因就是第一个失败的 Promise 的错误。其他还在进行的 Promise 并不会被取消,但他们的结果会被忽略。
典型场景:需要同时获取多个互不依赖的数据,并且要求“缺一不可”时使用。例如,加载用户信息和用户订单列表,两个请求必须都成功才能渲染页面。
const fetchUser = fetch('/api/user').then(res => res.json());
const fetchOrders = fetch('/api/orders').then(res => res.json());
Promise.all([fetchUser, fetchOrders])
.then(([user, orders]) => {
console.log('用户', user);
console.log('订单', orders);
})
.catch(err => {
// 任何一个请求失败都会进入这里
console.error('加载失败', err);
});
注意事项:
- 如果传入的数组中有非 Promise 值(比如数字、字符串),
Promise.all会直接将其视为已成功的 Promise 并保留原值。 - 如果其中一个 Promise 失败,整个批次即失败,无法获取其他成功的 Promise 的结果。若需要容忍部分失败,应改用
Promise.allSettled。 - 并发数量没有限制,但需注意被调用的下游服务的承载能力,高并发下可考虑搭配限流(见 11.2.5)。
11.2.2 Promise.allSettled:等待全部完成,不管成败
语法:Promise.allSettled(iterable)
参数:同 Promise.all。
返回值:一个新 Promise,在所有输入的 Promise 都已“敲定”(settled,即无论成功或失败)后,才变为 fulfilled。结果是一个对象数组,每个对象形如:
{ status: 'fulfilled', value: <结果> }{ status: 'rejected', reason: <错误原因> }
与 all 的区别:allSettled 不会因为某个 Promise 失败而拒绝整体,它始终等待全部完成,并且可以单独检查每个 Promise 的最终状态。
典型场景:批量处理任务,需要独立记录每个任务的结果,即使部分失败也要继续处理。例如,同时上传多个文件到不同服务器,最终汇总每个文件的上传状态,成功或失败都要明确告知用户。
const uploads = [
uploadFile('a.jpg'),
uploadFile('b.jpg'),
uploadFile('c.jpg')
];
Promise.allSettled(uploads).then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`文件 ${index} 上传成功: ${result.value}`);
} else {
console.error(`文件 ${index} 上传失败: ${result.reason}`);
}
});
});
注意事项:
allSettled永远返回一个成功的 Promise(除非参数不是可迭代对象从而直接报错),不会进入catch分支,需要自行处理内部的失败状态。- Node.js 12.9+ 原生支持
Promise.allSettled,更早版本可通过 polyfill 实现。
11.2.3 Promise.race:首个敲定的结果即刻返回
语法:Promise.race(iterable)
参数:可迭代对象。
返回值:一个新 Promise,它将采用 第一个敲定(settled) 的 Promise 的状态和值。如果第一个敲定的是成功,返回的 Promise 就成功;如果第一个敲定的是失败,返回的 Promise 就失败。
典型场景:设置超时竞速,或者从多个数据源中选用最快响应的那个。
最常用的例子是对一个异步请求添加超时控制:
const fetchData = fetch('/api/data');
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), 5000)
);
Promise.race([fetchData, timeout])
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
如果 5 秒内 fetchData 没有完成,timeout 会先变为 rejected,导致 race 返回一个拒绝 Promise,从而实现超时控制。
也可以用它实现从多个镜像地址加载同一份资源,选择最快那个:
const mirrors = [
fetch('https://cdn1.example.com/data.json'),
fetch('https://cdn2.example.com/data.json'),
fetch('https://cdn3.example.com/data.json')
];
Promise.race(mirrors)
.then(res => res.json())
.then(data => console.log('最快响应', data))
.catch(err => console.error('所有镜像都失败了或最先响应的是失败'));
注意事项:
race关心的是“谁先结束”,而非“谁先成功”。如果最快完成的 Promise 是失败的,整个race就会失败,即使后面有成功的也不会被采用。- 传入空数组时,
Promise.race([])会永远处于 pending 状态,因为没有 Promise 会敲定。
11.2.4 Promise.any:首个成功即返回,全部失败才报错
语法:Promise.any(iterable)
参数:可迭代对象。
返回值:一个新 Promise。
- 只要 任意一个 传入的 Promise 变为
fulfilled,返回的 Promise 会立即采用该值,并忽略其他还在进行中的 Promise。 - 如果 所有 传入的 Promise 都变为
rejected,返回的 Promise 会变为rejected,并提供一个AggregateError类型的错误,其中包含每个失败的详细信息。
与 race 的区别:race 关注第一个完成(不论成败),any 关注第一个成功,会忽略那些失败的 Promise(除非全部失败)。
典型场景:从多个冗余资源加载数据,只要有一个可用即可。例如,从多个 CDN 获取某个静态文件,或从多个微服务实例请求相同数据,只要任意一个返回成功结果,即可继续。
const backupRequests = [
fetch('https://primary.api/data'),
fetch('https://secondary.api/data'),
fetch('https://fallback.api/data')
];
Promise.any(backupRequests)
.then(res => res.json())
.then(data => {
console.log('成功获取数据', data);
})
.catch(err => {
// 只有全部请求都失败才会到这一步
console.error('所有数据源都不可用', err);
// err.errors 数组包含每个请求的错误
});
注意事项:
Promise.any在 Node.js 15+ 中才得到原生支持,早期版本可引入 polyfill。- 如果传入空数组,
Promise.any([])会直接拒绝并抛出AggregateError,因为没有 Promise 能成功。
11.2.5 实际项目中的组合与取舍
这四种方法并不是孤立的,经常需要按场景组合使用。例如:
- 批量请求中,用
Promise.all保证数据完整性,但如果涉及外部不可靠服务,可能会因一个失败而阻塞整个流程,此时可在每个独立请求上使用.catch给一个默认值,使整体仍然能够成功。 - 并发数量过大时,直接用
Promise.all可能会压垮数据库或下游服务,需要结合并发限流,比如每次只启动 N 个请求。常见的做法是使用p-limit这类库,或手动用Promise队列控制。 - 超时控制经常和
Promise.race搭配,但需要注意:即使race返回了超时错误,底层的异步操作(如 HTTP 请求)仍然在进行,可能会干扰后续逻辑。最佳实践是配合AbortController取消底层操作。
代码示例:带超时和取消的 race + AbortController
const controller = new AbortController();
const signal = controller.signal;
const fetchTask = fetch('/api/data', { signal });
const timeout = new Promise((_, reject) => {
const id = setTimeout(() => {
controller.abort(); // 主动取消请求
reject(new Error('请求超时'));
}, 5000);
// 如果请求先完成,清除定时器
fetchTask.finally(() => clearTimeout(id));
});
Promise.race([fetchTask, timeout])
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.error('请求被取消');
} else {
console.error(err);
}
});
11.2.6 总结对比
| 方法 | 解决时机 | 拒绝时机 | 适用场景 |
|------|----------|----------|----------|
| Promise.all | 所有 Promise 都成功 | 任意一个失败 | 需要所有结果,缺一不可 |
| Promise.allSettled | 所有 Promise 都敲定(无论成败) | 永不拒绝(除非参数错误) | 批量任务独立记录结果 |
| Promise.race | 第一个敲定的 Promise(无论成败) | 第一个敲定的如果是拒绝 | 超时控制,最快响应 |
| Promise.any | 第一个成功的 Promise | 全部失败 | 冗余请求,最快成功 |
选择哪一种,完全取决于你希望程序如何处理部分失败以及何时进入下一步。理解这些方法的语义,能够让你的异步流程控制代码更清晰、更健壮,避免陷入手动维护状态和回调嵌套的泥潭。