别再串行写 await 了,Promise.all 才是并行请求的正确打开方式

好好的并行请求,被我写成了串行

前几天写个小 demo 页面,要同时拉一句诗词一言和模拟的天气数据,我顺手写了两个 await 串着跑,跑起来总觉得页面加载慢吞吞的。打开控制台掐了个时间,好家伙,两个接口加起来耗了快 2.5 秒,可明明单个请求最慢也就 2 秒。

javascript 复制代码
function getWeather() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ temp: 38, conditions: 'Sunny with Clouds'})
    }, 2000) // 模拟 2 秒返回
  })
}
function getTweets() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(['I like cake', 'BBQ is good too!'])
    }, 500) // 模拟 500 毫秒返回
  })
}

async function main() {
  console.time("my-operation");
  const weatherData = await getWeather();
  const tweetsData = await getTweets();
  console.log(weatherData);
  console.log(tweetsData);
  console.timeEnd("my-operation");
  // 当时跑出来大概 2.5s 左右
}

现在回头看,这写法纯纯浪费时间。两个请求八竿子打不着,谁也不依赖谁的返回值,凭啥要等天气数据拿到了,才去发第二条请求?但写的时候就是顺手,await 一行行往下敲,完全没过脑子,估计很多人刚开始写异步代码的时候都干过这事。

改完 Promise.all,耗时直接砍了近一秒

反应过来之后我立刻改成了 Promise.all 的写法,把两个请求塞到一个数组里传进去,再统一 await。

javascript 复制代码
    const main = async () => {
      console.time("my-operation");
      // 两个请求一起发
      const [weatherData, tweetsData] = await Promise.all([getWeather(), getTweets()]);
      console.log(weatherData);
      console.log(tweetsData);
      console.timeEnd("my-operation");
      // 跑出来大概 2s 左右,刚好等于最慢的那个请求
    }

跑完一看耗时,2 秒出头,直接省了 500 毫秒。

说实话这个道理我之前也听过,"Promise.all 可以并行执行异步任务,总耗时等于最慢的那一个"。但真到自己写代码的时候,脑子一热就写成串行的了。

你可以把 Promise.all 理解成一个小组作业的组长:你把好几件任务(Promise)扔给它,它就让所有人同时开工。等所有人都交作业了,它就把结果打包好一起给你;但凡有一个人摆烂说做不完,它直接就宣告整个小组任务失败,剩下的人做没做完它都不等了。

每个 Promise 生下来都是 pending(待处理)状态,要么成功变成 fulfilled,要么失败变成 rejected,一旦状态定死了就再也改不了。Promise.all 干的事,就是盯着这一堆 Promise 的状态,等它们全部变成 fulfilled,或者有任何一个变成 rejected,它就给出最终结果。

我踩的第一个坑:结果顺序居然不是谁快谁在前

改完代码我盯着输出看了半天,突然犯了个嘀咕:getTweets 500 毫秒就返回了,getWeather 要 2 秒,按道理快的应该先到吧?怎么结果数组里第一个永远是 weather,第二个才是 tweets?

我当时还以为自己写反了,来回调换了三次数组里的顺序,最后才确认:结果的顺序,和 Promise 完成的先后半毛钱关系都没有,只和你传进数组的顺序一一对应

javascript 复制代码
    // 哪怕 tweets 先跑完,结果数组里第 0 位永远是 weather 的结果
    const [weatherData, tweetsData] = await Promise.all([getWeather(), getTweets()]);
    // 别问我为什么知道,试了三次才敢确定

后来想通了也合理,要是顺序按完成时间来排,那代码根本没法写 ------ 你永远不知道拿到的数组里第几个对应哪个请求,总不能每次都去判断字段吧。这个设计看起来 "反直觉",实际上是帮我们省了好多麻烦。

一个容易忽略的细节哪怕某个任务 10 毫秒就返回了,另一个要 10 秒,最终结果数组里的顺序,还是和你传进 Promise.all 的顺序完全一致。写解构赋值的时候放心按传入顺序写就行。

第二个坑:一个请求炸了,所有结果都拿不到

顺顺利利用了两天,我又踩了第二个坑。

那天我在调一言的接口,写了两个并行请求,手滑把其中一个 URL 写错了。本来我以为,错的那个报错就报错呗,另一个正常的总该能拿到数据吧?结果控制台直接红了,整个 Promise.all 直接走进了 catch,成功的那个请求的数据连影子都没见着。

javascript 复制代码
    const getStory = async () => fetch('https://v1.hitokoto.cn/?c=i&encode=json');
    const getError = async () => fetch('https://不存在的地址.cn/'); // 故意写错的

    Promise.all([getStory(), getError()])
      .then(res => {
        console.log('成功了', res);
      })
      .catch(err => {
        console.log('失败了', err);
        // 只要有一个失败,直接走到这里,成功的结果也拿不到
      });

当时我懵了,不至于这么极端吧?一个失败而已,怎么连成功的都不给我了?

翻了下规范才确认,这就是 Promise.all 的核心规则:全成才成,一败就败 。只要数组里有任何一个 Promise 变成 rejected 状态,整个 Promise.all 就会立刻 rejected,直接把第一个失败的原因抛给 catch,剩下还在执行的 Promise 它也不会等了。

我笔记里记的那句 "只要有一个失败,整体失败,不再等待其他 promise 执行,走 catch 第一个失败的原因",就是踩完这个坑之后补上的。光看文档没感觉,真踩一次就记牢了。

再往深扒一层:Promise 什么时候开始执行?

踩完两个坑之后我就开始好奇一件事:这些 Promise,到底是调用 Promise.all 的时候才开始执行,还是更早?

之前我一直想当然地觉得,肯定是 Promise.all 拿到数组之后,才挨个启动里面的任务。直到有次我随手把 Promise 提前创建了,过了一秒才塞进 Promise.all,结果总耗时居然没变。

javascript 复制代码
    // 函数一调用,Promise 就已经开始执行了
    const p1 = getWeather();
    const p2 = getTweets();

    // 这里同步代码执行了 1 秒(只是举例子,别真写死循环)
    // ...

    // 1 秒后才调用 Promise.all
    const res = await Promise.all([p1, p2]);
    // 总耗时还是 2 秒左右,因为任务早就开始跑了

后来才搞明白:Promise 在创建的那一刻就已经开始执行了new Promise 里面的代码是同步执行的,异步操作(比如 setTimeout、请求)也会立刻发起。Promise.all 啥都没干,它只是个 "等结果的人",不是 "发任务的人"。

这个点很多人都容易搞错,以为把 Promise 放数组里存着就不会执行,等 all 的时候才开始。其实不是,你只要调用了返回 Promise 的函数,任务就已经发出去了。

批量请求的常见写法:别忘了二次 Promise.all

平时开发里最常用的场景,其实是批量发请求,然后批量解析返回结果。比如用 fetch 发请求,拿到的 Response 对象不是直接的 JSON 数据,得调用 .json() 方法,而这个方法本身也返回一个 Promise。

我第一次写批量 fetch 的时候,就踩了这个坑:

javascript 复制代码
    // 错误写法
    const resList = await Promise.all([getStory(), getRatp()]);
    const dataList = resList.map(item => item.json());
    console.log(dataList); 
    // 打印出来全是 Promise 对象,不是数据
    // 我当时盯着屏幕看了三分钟,心想我不是 await 过了吗?

拍了下脑袋才反应过来:.json() 本身就是异步的啊!map 遍历完得到的还是一堆 Promise,当然得再用一次 Promise.all 等它们全部解析完。

正确的写法应该是这样:

javascript 复制代码
    const getStory = async () => fetch('https://v1.hitokoto.cn/?c=i&encode=json');
    const getRatp = async () => fetch('https://v1.hitokoto.cn/?c=i&encode=json');

    Promise.all([getStory(), getRatp()])
      .then(res => {
        // 把所有 response 转成 json,再用 Promise.all 等全部解析完
        return Promise.all(res.map(item => item.json()));
      })
      .then(dataList => {
        console.log(dataList);
        // 这里拿到的才是真正的 JSON 数据数组
      })
      .catch(err => {
        console.log(err);
      });

如果你喜欢用 async/await,也可以写成两行 await:

javascript

运行

javascript 复制代码
async function main() {
  const resList = await Promise.all([getStory(), getRatp()]);
  const dataList = await Promise.all(resList.map(item => item.json()));
  console.log(dataList);
}

两种写法本质一样。核心就是记住:只要里面还有 Promise,就别想当然地以为拿到了最终值,该 all 还得 all

最后聊聊,什么时候该用,什么时候别乱用

其实 Promise.all 本身不难,语法也简单,难的是别用错场景。

适合用的场景其实很明确:好几个异步任务互相没有依赖关系,你必须等所有任务都完成了才能往下走。比如页面初始化的时候,同时拉取用户信息、列表数据、配置项,这几个接口谁也不依赖谁,用 Promise.all 并行发,能明显减少页面加载时间。还有批量上传文件、批量查询数据之类的,都很合适。

但有几种情况就别硬用了:

  • 任务之间有依赖,第二个请求需要第一个的返回值,老老实实串行写 await,别为了炫技搞并行。
  • 你不关心失败,只想拿到所有成功的结果,失败的忽略掉。这种情况用 Promise.allSettled 更合适,别硬用 Promise.all 然后自己写一堆 catch 容错。
  • 你只需要最快的那个结果,剩下的无所谓。那直接用 Promise.race,谁先回来用谁。

说实话,这些东西看文档的时候都觉得 "这不废话吗",真自己写代码,该踩的坑一个都少不了。很多细节都是摔过一次之后,才真正刻进脑子里。

你们平时写 Promise 的时候,踩过什么印象特别深的坑?评论区说出来,大家一起避避坑。

相关推荐
To_OC2 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic2 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy2 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志3 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble4 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长4 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯5 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜5 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司6 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能