JavaScript 并发请求入门:Promise.all 和 Promise.allSettled 到底怎么选?

如果你在写代码时遇到过"同时发起好几个请求,等它们都完成后再做点什么"的场景,那你一定会用到 Promise.all 或者 Promise.allSettled。这篇教程会用最简单的例子,带你一步步搞懂这两个 API 的区别、原理和实际用法。


一、先聊聊为什么需要它们

假设你要做一个用户主页,页面上需要展示:

  • 用户基本信息
  • 用户的订单列表
  • 用户的消息通知

这三个数据分别来自三个不同的接口。如果你这样写:

javascript 复制代码
const user = await fetch('/api/user');
const orders = await fetch('/api/orders');
const messages = await fetch('/api/messages');

问题来了:这三个请求是一个接一个发出去的。假设每个请求耗时 1 秒,那么总共要等 3 秒页面才能渲染完。

但其实这三个请求互不依赖 ,完全可以同时发出去,谁先回来不影响谁。这时候我们就需要"并发"请求多个 Promise,而 Promise.allPromise.allSettled 就是干这个的。


二、Promise.all:一个失败,全部失败

2.1 基本用法

javascript 复制代码
const p1 = fetch('/api/user');
const p2 = fetch('/api/orders');
const p3 = fetch('/api/messages');

const results = await Promise.all([p1, p2, p3]);
// results 是一个数组:[userResponse, ordersResponse, messagesResponse]

Promise.all 接收一个 Promise 数组,同时 发起所有请求,等它们全部成功后,把结果按顺序打包成一个数组返回给你。

💡 注意:返回结果的顺序和你传入数组的顺序是一致的,跟哪个请求先完成没关系。

2.2 关键特性:快速失败(Fail-Fast)

这是新手最容易踩的坑。只要数组里有任何一个 Promise 失败(reject),Promise.all 会立刻失败,哪怕其他 9 个请求都已经成功了。

来看一个例子:

javascript 复制代码
function fakeRequest(name, delay, shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error(`${name} 请求失败了`));
      } else {
        resolve(`${name} 请求成功`);
      }
    }, delay);
  });
}

async function run() {
  try {
    const results = await Promise.all([
      fakeRequest('用户信息', 1000, false),
      fakeRequest('订单列表', 500, true),  // 这个会失败
      fakeRequest('消息通知', 1500, false),
    ]);
    console.log('全部成功:', results);
  } catch (err) {
    console.log('捕获到错误:', err.message);
    // 输出: 捕获到错误: 订单列表 请求失败了
  }
}

run();

运行结果分析:

  • "订单列表"请求在 500ms 时失败了
  • Promise.all 立刻抛出错误,不会等待"用户信息"(1000ms)和"消息通知"(1500ms)完成
  • 你在 catch 里只能拿到第一个失败的错误信息,另外两个请求即使最终成功了,结果也被"扔掉了",你根本拿不到

2.3 什么时候用 Promise.all?

适合所有任务必须全部成功,少一个都不行的场景:

  • 页面初始化时必须同时拿到"用户身份"和"权限列表",少了权限列表页面根本没法渲染
  • 提交表单时需要同时上传多张图片,任何一张失败整个提交都应该失败

三、Promise.allSettled:不管成功失败,都等你说完

3.1 基本用法

javascript 复制代码
const results = await Promise.allSettled([p1, p2, p3]);

Promise.allSettled 也是同时发起所有请求,但它不会因为某个请求失败就提前结束 ,而是耐心等所有 Promise 都有了结果(无论成功还是失败),然后把每一个的结果都告诉你。

3.2 返回结果的格式

这是新手第二个容易搞混的地方:allSettled 返回的数组,每一项不是 直接的结果值,而是一个带有 status 字段的对象:

javascript 复制代码
// 成功的项长这样:
{ status: 'fulfilled', value: '用户信息 请求成功' }

// 失败的项长这样:
{ status: 'rejected', reason: Error('订单列表 请求失败了') }

用同样的例子跑一遍:

javascript 复制代码
async function run() {
  const results = await Promise.allSettled([
    fakeRequest('用户信息', 1000, false),
    fakeRequest('订单列表', 500, true),  // 这个会失败
    fakeRequest('消息通知', 1500, false),
  ]);

  console.log(results);
  /* 输出:
  [
    { status: 'fulfilled', value: '用户信息 请求成功' },
    { status: 'rejected', reason: Error('订单列表 请求失败了') },
    { status: 'fulfilled', value: '消息通知 请求成功' }
  ]
  */
}

run();

看到了吗?即使"订单列表"失败了,"用户信息"和"消息通知"的结果我们依然拿到了。这就是 allSettled 最大的价值------它不会因为一个失败,就把其他成功的结果一起丢掉

3.3 如何处理 allSettled 的结果

因为返回的是带 status 的对象数组,你需要手动遍历,分别处理成功和失败的情况:

javascript 复制代码
const results = await Promise.allSettled([p1, p2, p3]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`第 ${index} 个请求成功,结果是:`, result.value);
  } else {
    console.log(`第 ${index} 个请求失败,原因是:`, result.reason);
  }
});

也可以用 filter 分别把成功和失败的结果提取出来:

javascript 复制代码
const succeeded = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

const failed = results
  .filter(r => r.status === 'rejected')
  .map(r => r.reason);

console.log('成功的结果:', succeeded);
console.log('失败的原因:', failed);

3.4 什么时候用 Promise.allSettled?

适合任务之间互相独立,允许部分失败的场景:

  • 页面上有 3 个独立的卡片组件,各自请求各自的数据,其中一个接口挂了,不应该影响另外两个卡片正常显示
  • 批量上传多个文件,需要告诉用户"哪些成功了,哪些失败了",而不是一失败就全部作废
  • 埋点、日志上报等允许失败、但不能因为失败而中断主流程的场景

四、两者对比总结

对比项 Promise.all Promise.allSettled
何时结束 任意一个 reject 就立刻结束 等所有 Promise 都有结果才结束
一个失败,其他会怎样 其他的结果全部丢失,拿不到 其他成功/失败的结果都会保留
返回值格式 直接是结果值数组 {status, value}{status, reason} 的对象数组
需要 try/catch 吗 需要,失败会抛异常 不需要,永远 resolve,自己判断 status
典型场景 强依赖,少一个都不行 弱依赖,允许部分失败

一句话记忆法:

all 是"团灭模式"------一个倒下,全部阵亡;allSettled 是"点名模式"------不管死活,每个人都要报到。


五、实战:一个更接近真实业务的例子

假设你要做一个仪表盘页面,同时请求 3 个统计接口来渲染 3 张图表卡片,任何一张图表挂了都不该影响另外两张正常展示:

typescript 复制代码
interface CardData {
  title: string;
  data: unknown;
  error: string | null;
}

async function loadDashboard(): Promise<CardData[]> {
  const requests = [
    { title: '销售额趋势', fetcher: () => fetch('/api/sales').then(r => r.json()) },
    { title: '用户增长', fetcher: () => fetch('/api/users').then(r => r.json()) },
    { title: '订单统计', fetcher: () => fetch('/api/orders').then(r => r.json()) },
  ];

  const results = await Promise.allSettled(
    requests.map(req => req.fetcher())
  );

  // 把 settled 结果转换成统一的、方便渲染的格式
  return results.map((result, index) => ({
    title: requests[index].title,
    data: result.status === 'fulfilled' ? result.value : null,
    error: result.status === 'rejected' ? String(result.reason) : null,
  }));
}

这样一来,即使"订单统计"接口挂了,页面依然能正常渲染"销售额趋势"和"用户增长"两张卡片,只在"订单统计"的位置显示一个错误提示,而不是整个仪表盘白屏。


六、新手常踩的 3 个坑

坑 1:以为 allSettled 会像 all 一样抛错

javascript 复制代码
// ❌ 错误认知:以为失败了会走 catch
try {
  const results = await Promise.allSettled([p1, p2]);
} catch (err) {
  // 这里永远不会被触发!allSettled 不会 reject
}

allSettled 永远不会 reject ,你必须手动检查每一项的 status,漏检查就等于"悄悄吞掉了错误"。

坑 2:用 Promise.all 却没做好整体降级处理

如果你的业务场景其实允许部分失败,却用了 Promise.all,一旦某个请求偶发失败,就会导致整个功能不可用,体验非常差。先想清楚业务上是"强依赖"还是"弱依赖",再选 API

坑 3:忘记超时控制

无论是 all 还是 allSettled,如果某个 Promise 一直不 resolve/reject(比如网络长时间无响应),都会一直卡住不返回。生产环境建议自己包一层超时:

typescript 复制代码
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error('请求超时')), ms);
  });
  return Promise.race([promise, timeout]);
}

// 使用时给每个请求都包一层超时
const results = await Promise.allSettled([
  withTimeout(fetch('/api/sales'), 5000),
  withTimeout(fetch('/api/users'), 5000),
]);

七、总结

  • Promise.all :适合"必须全成功"的强依赖场景,一个失败全部作废,写法简单(直接 try/catch 即可)。
  • Promise.allSettled :适合"允许部分失败"的弱依赖场景,不管成功失败都会等你,需要自己遍历判断 status
相关推荐
不好听6132 小时前
BFF 架构实战:从前端直调 API 到加入中间层
前端·架构
不好听6132 小时前
前端跨域完全指南:从为什么报错到三种解决方案
前端
三8442 小时前
路由策略/控制 配置双点双向路由重发布
服务器·前端·javascript
Qimooidea3 小时前
祁木 CAD Translator 工程图纸出海实战指南
服务器·前端·安全
小林ixn3 小时前
从BFF到SSE:我在Vue项目里藏了个“AI翻译官”
前端·vue.js·vite
dtq04243 小时前
C语言-结构体详解
c语言·开发语言·学习
梅雅达编程笔记3 小时前
编程启蒙|Scratch 转 Python 系列第9天:字典/哈希表积木双向对照(AI大模型参数配置表实战)
开发语言·人工智能·python·numpy·pandas
持力行3 小时前
C++与Java变量声明、定义及内存分配的核心区别
java·开发语言·c++
元气少女小圆丶4 小时前
unity发布web嵌入到前端页面的接受参数
前端·unity·webgl
工业HMI实战笔记4 小时前
【拯救HMI】:低碳制造:自动化技术如何助力企业节能降耗
运维·前端·自动化·交互·制造