人人都会AI编程

11.2 异步并发工具

更新时间:2026-07-10

在实际开发中,经常需要同时发起多个异步操作(比如并发请求多个 API、读取多个文件),并根据它们的结果执行后续逻辑。Node.js 完全支持 ECMAScript 的 Promise 静态方法,这些工具可以帮助我们用简洁的代码控制并发、收集结果、处理异常,无需手动编写计数器或状态跟踪。本节逐个介绍 Promise.allPromise.allSettledPromise.racePromise.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 | 全部失败 | 冗余请求,最快成功 |

选择哪一种,完全取决于你希望程序如何处理部分失败以及何时进入下一步。理解这些方法的语义,能够让你的异步流程控制代码更清晰、更健壮,避免陷入手动维护状态和回调嵌套的泥潭。