js如何在并发中同步两个fetch请求(附 3 种方法)

编程狮(w3cschool.cn) 2026-10-17 07:04:57 浏览数 (21)
反馈

在 JavaScript 里让两个 fetch 并发请求同时发出、再一起拿到结果,标准做法是 Promise.all:把两个请求放进数组一次性发起,用一次 await 收齐。想允许其中一个失败,改用 Promise.allSettled。真正的坑是有人写成两次顺序 await,那不是并发,是串行,耗时等于两者之和。

两个 fetch 怎么并发附 3 种写法

初学者最容易踩的是后面这个坑。代码看起来很「同步」,写起来也自然,但它把两个本可以并行的网络请求变成了排队执行,页面加载时间直接翻倍。而且因为没有对比过耗时,这个性能问题往往上线很久才被发现。

本文给出三种写法、一份横向对比表、超时控制的做法,以及判断自己写的是并发还是串行的具体方法。今天这篇文章,编程狮就把这块讲透。

先看结论

两个 fetch 要并发执行并一起拿结果,用 Promise.all;要允许部分失败,用 Promise.allSettled;顺序 await 是串行写法,不该出现在并发场景里。

判断并发还是串行,看发起时机:两个 fetch 调用是否在同一轮同步代码里就都执行了。只要第二个 fetch 写在第一个 await 之后,它就是串行的。

异步这块若不牢,可以先翻 JavaScript 教程 把 Promise 的三种状态过一遍,再回来看这三种写法。

一、先分清「并发」和「同步等待」

这两个词说的是两件事,混在一起就容易写错。

1.1 并发指的是发起时机

并发是说两个请求几乎同时发出去,网络层面重叠,总耗时接近两者中较慢的那个。

1.2 同步等待指的是收结果的方式

同步等待是说你用 await 把结果收齐再往下走,代码读起来像同步代码。它不关心两个请求是怎么发出的。

所以「并发 + 同步等待」的正确组合是:先一次性发起,再一次性等待。写成「发起 → 等待 → 再发起 → 再等待」,就变成了串行。

写法 发起方式 总耗时 一个失败时
Promise.all 并发 取较慢者 整体失败
Promise.allSettled 并发 取较慢者 其余照常返回
顺序 await 串行 两者之和 中断后续

二、方法一:Promise.all,都成功才继续

两个请求必须都成功才有意义时,用这个。

async function loadBoth(userId) {
  const [user, order] = await Promise.all([
    fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
    fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
  ]);
  return { user, order };
}

loadBoth(42).then(console.log).catch(console.error);

上面这段代码做的是:两个 fetch 在数组里被同时构造并发出,Promise.all 等两个都完成后按数组顺序返回结果,再用解构分别取出。注意 .then((r) => r.json()) 不能漏,忘了这步拿到的是 Response 对象而不是数据。

⚠️ 注意:Promise.all 是「一失败全失败」。只要其中一个 reject,整个 Promise 就 reject,另一个请求的结果也拿不到。需要部分成功时不要用这个。

三、方法二:Promise.allSettled,允许部分失败

两个请求互相独立、一个挂了也不能影响另一个时,用这个。

async function loadBothSafe(userId) {
  const results = await Promise.allSettled([
    fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
    fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
  ]);

  const picked = results.map((item) =>
    item.status === 'fulfilled' ? item.value : { error: String(item.reason) }
  );
  return { user: picked[0], order: picked[1] };
}

上面这段代码做的是:等两个请求都结束(无论成功失败),逐个判断状态,成功取 value,失败把原因转成字符串。这样界面上可以只降级显示失败的那一块。

💡 小提示:失败原因转成字符串再往上抛,比直接抛原始错误更容易在界面上展示,也避免把内部堆栈暴露给用户。

四、超时控制与三个高频坑

并发写法本身不难,难在边界。

4.1 加超时:用 AbortController

网络请求没有默认超时,一个卡住的请求会让 Promise.all 永远挂着。

async function fetchWithTimeout(url, ms) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), ms);
  try {
    const resp = await fetch(url, { signal: ctrl.signal });
    return await resp.json();
  } finally {
    clearTimeout(timer);
  }
}

上面这段代码做的是:创建一个中止控制器,设定毫秒后触发中止,把信号传给 fetch,无论成功失败都在最后清掉定时器。预期结果(未执行,依据 AbortController 的标准行为)是超时时抛出一个中止错误。

4.2 坑一:顺序 await 当成并发

修复方式是把两个 fetch 调用放进同一个数组,不要在中间 await。

4.3 坑二:忘了处理 HTTP 错误状态

fetch 只在网络层面失败时 reject,404、500 这类状态码不会触发 reject。需要自己判断 resp.ok。

4.4 坑三:在循环里 await

循环里逐个 await 是典型的串行写法,请求多了耗时会线性增长。批量场景仍然要用 Promise.all,但要控制并发数量,避免一次性打出几百个请求。

两个 fetch 并发请求的三种写法对比

4.5 本地跑一遍,验证到底是不是并发

只靠读代码判断并发不可靠,跑一次测耗时最直观。下面这段做的是分别用两种写法各跑一遍并打印耗时。

async function bench(userId) {
  const t0 = Date.now();
  await Promise.all([
    fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
    fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
  ]);
  console.log('Promise.all 耗时', Date.now() - t0, 'ms');

  const t1 = Date.now();
  await fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json());
  await fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json());
  console.log('顺序 await 耗时', Date.now() - t1, 'ms');
}

上面这段代码做的是分别测量两种写法的总耗时。预期结果(未执行,依据并发与串行的执行模型)是顺序写法的耗时接近两次请求之和,并发写法接近其中较慢的那一次。

在 Node 环境里运行这段脚本的命令如下(预期结果:打印两组耗时数字)。

node -v
node bench.js

上面两条命令做的是先检查 Node 版本,再执行脚本。本机只需要安装 Node 运行环境,不需要额外依赖;预期结果(未执行)是终端打印出两组毫秒数,两组差距明显就说明并发写法确实生效了。

总结

两个 fetch 并发请求要一起收结果,首选 Promise.all;允许部分失败就用 Promise.allSettled;顺序 await 是串行写法,别用在并发场景。另外记得补超时与 HTTP 状态判断,这两项是线上事故的高发点。

要点带走:

  • 判断并发看发起时机,第二个 fetch 写在第一个 await 之后就是串行;
  • Promise.all 一失败全失败,需要部分成功要换成 Promise.allSettled;
  • fetch 不会因 404、500 而 reject,状态码要自己判断。

下一步建议给自己封装一个带超时和状态检查的请求函数,后面所有页面都复用它。语法细节记不住时,翻 JavaScript 速查手册 最快。

加上超时控制后的请求执行流程

常见问题

Q:Promise.all 和 Promise.allSettled 该怎么选?

A:两个请求必须都成功才继续,用 Promise.all;一个挂了也要展示另一块,用 allSettled。

Q:为什么 fetch 返回 404 但代码没进 catch?

A:fetch 只在网络层面失败时 reject,HTTP 错误状态码不算失败。需要自己判断 resp.ok 并主动抛错。

Q:并发请求数量很多怎么办?

A:一次性打出几百个请求会拖垮服务端,也可能触发浏览器限制。需要自己实现分批并发,控制同时在飞的请求数。

Q:怎么确认自己写的是并发不是串行?

A:看两个 fetch 是否在同一轮同步代码里被构造。也可以在控制台计时,串行耗时约等于两者之和。

延伸学习

想把异步这块补齐,可以按这个顺序来:

  1. 读一篇相关笔记 JavaScript 异步编程,把 Promise 这条线理顺;
  2. 想动手练,前端配套课程 是边学边写的形式;
  3. 想再往网络层深入,可以看 WebSocket 协议教程。

0 人点赞