Promise.all 并发请求实战:从串行 await 到双段并行

写在前面

async/await 写起来是真爽------一个 await 接一个 await,异步代码写得像同步。

但你接口一多就翻车:5 个接口串行 await,用户等你 5 段时间叠加。明明这 5 个接口互不依赖,凭什么排队?

答案就在 Promise.all。今天用一段真实能跑的 fetch 双接口并发代码,把 Promise 三态、一败全败、双段并行全串一遍。

Promise 三态:pending / fulfilled / rejected

new Promise 一执行,立马进入 pending(待处理)。然后只能走到两个终点之一:

状态 含义 触发 能不能回退
pending 待处理,还没结果 刚 new 出来 可以走
fulfilled 成功 resolve() 不能回 pending
rejected 失败 reject() 不能回 pending,也不能变 fulfilled

关键一条:状态只能从 pending → fulfilled,或 pending → rejected,一旦定了就不能再变。 这就是为什么 Promise 叫"承诺"------兑现就兑现了,黄了就黄了,没得反悔。

控制台里你看到的 Promise {<pending>} 就是这个状态机的外显。等结果回来,要么变 <resolved>,要么变 <rejected>

一个失败的连锁反应:rejected 一败全败

Promise.all 有条铁律:数组里只要有一个 Promise rejected,整体立刻 rejected

而且它不等其他 Promise 跑完------剩下的该 pending 还 pending,结果直接丢。.catch 拿到的,是第一个失败的原因

场景 Promise.all 行为
全部 fulfilled 返回结果数组,顺序 = 传入顺序
一个 rejected 整体 rejected,走 catch,拿第一个失败原因
多个 rejected 还是走 catch,只给第一个
部分还在 pending 不影响------它只看有没有 reject,不看进度

这条规则决定了:Promise.all 的前提是"所有请求必须成功" 。只要有一个接口挂了,整个并行批次全废。如果你能容忍部分失败,得换 Promise.allSettled

串行 await vs Promise.all 并行

先看一段真实能跑的代码,两个互不依赖的接口:

js

javascript 复制代码
const getStory = async () =>
  fetch('https://v1.hitokoto.cn/?c=i&encode=json');   // 一言 API
const getRatp = async () =>
  fetch('https://api.1314.cool/bingimg/?type=json&rand=1');  // 必应每日图

串行写法(天真版):

js

csharp 复制代码
async function main() {
  const story = await getStory();   // 等 hitokoto 回来
  const ratp = await getRatp();     // 再等 bingimg 回来
  // 总耗时 ≈ T1 + T2
}

两个接口各 500ms,串行就是 1000ms。第二个接口明明可以和第一个同时跑,却被你按住排队了。

并行写法(Promise.all):

js

javascript 复制代码
async function main() {
  const [storyRes, ratpRes] = await Promise.all([
    getStory(),
    getRatp(),
  ]);
  // 总耗时 ≈ max(T1, T2)
}
写法 总耗时 结果顺序
串行 await T1 + T2 按代码顺序
Promise.all max(T1, T2) 传入数组顺序

重点Promise.all 的结果顺序,跟谁先完成无关 ------它按你传入数组的顺序收集。bingimg 比 hitokoto 先回来?没关系,结果数组里 hitokoto 还是排第一。这个特性让你能放心解构 [storyRes, ratpRes]

嵌套 Promise.all:双段并行

光 fetch 并发还不够。fetch 返回的是 Response 对象,你得再调 .json() 才能拿到数据------而 .json() 本身也是个 Promise。

笔记里的写法很妙,第二段也并行

js

ini 复制代码
Promise.all([getStory(), getRatp()])
  .then(response => {
    return Promise.all(response.map(res => res.json()));
  })
  .then(([storyData, imgData]) => {
    console.log(storyData, imgData);
  })
  .catch(err => {
    console.log('某一个挂了:', err);
  });

拆开看两段并行:

阶段 干了啥 并行对象
第一段 Promise.all 同时发两个 fetch 请求 [getStory(), getRatp()]
.then 里第二段 同时解析两个 response 的 json response.map(res => res.json())

response.map(res => res.json()) 这行是精髓------map 把每个 Response 转成 res.json() 这个 Promise,得到一个 Promise 数组,再喂给 Promise.all

两段并发接力,整条链路上没有一处是"傻等"。这才是 Promise.all 的正确打开方式。

5 个踩坑提醒

1. 以为 .thenres.json() 是同步的。 它返回 Promise!不 await 或不包进 Promise.all,拿到的就是个 Promise 对象,不是数据。

2. 串行 await 写成"假并行"。 await getStory(); await getRatp(); 看着像并行,其实是串行------两个 await 中间隔着等。要并行得用 Promise.all,或先 const p1 = getStory(); const p2 = getRatp(); 再分别 await。

3. 接口容错用错方法。 Promise.all 一个失败全废。如果你要"成功的拿数据、失败的给默认值",换 Promise.allSettled,它返回 [{status, value/reason}, ...],自己判断。

4. 结果顺序搞混。 别以为"谁先回来谁排前面"------Promise.all 严格按传入数组顺序。想按完成顺序处理,得用 Promise.race 或自己手写。

5. 忘了 catch。 Promise.all 一旦 reject 不接 .catch,错误就丢了,控制台报 UnhandledPromiseRejection。生产环境必接 catch,哪怕只是打日志。

写在最后

Promise.all 不是"快一点"那么简单,它是一种任务依赖关系的表达------"这堆事互不依赖,谁也别等谁"。串行 await 是"我非得等完 A 才开始 B",并行是"你们一起去,我等最慢的那个回来"。

相关推荐
IT_陈寒几秒前
Java序列化坑了我三天,原来问题出在这个默认方法
前端·人工智能·后端
雪芽蓝域zzs8 分钟前
第四十八节:驾驶舱大屏进阶:数字滚动动画 + 渐变边框美化
前端·javascript·vue.js
ly768913 分钟前
Spring Boot 3.2 虚拟线程在 Web 层与 JDBC 池的实践边界
前端·spring boot·后端·tomcat·jdbc·虚拟线程
Bingo_BIG16 分钟前
LOV列表的查询
前端·javascript·lov
Setsuna_F_Seiei7 小时前
前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力)
前端·agent
刘发财9 小时前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理9 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡10 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai11 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
计算机魔术师12 小时前
特朗普上台打给黄仁勋:AI末日论是骗局,我们绝不让它发生
前端