JS 代码技巧 vol.4 — 10 个异步并发控制,Promise.all 这帮兄弟的踩坑实录

JS 代码技巧 vol.4 --- 10 个异步并发控制,Promise.all 这帮兄弟的踩坑实录

小不的代码技巧系列第 4 期

主题:异步并发控制

这期是真的"踩坑里淌过来的血"------并发这事,写对是性能,写错是玄学 😎

哈喽哇!我是小不 ,不简说的不~

作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂

今天这期,主打一个 异步并发控制------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱

Ps:文末有惊喜(不是广告!)


1. Promise.all:一个失败全盘皆输

经典场景:批量上传 10 个文件,3 个失败,剩下 7 个其实成功的------但用户啥都看不到。

js 复制代码
// ❌ 一个 reject,全部 reject
const results = await Promise.all(files.map(upload));
// 只要 upload 中任何一个 throw,外层 await 直接抛错

正解------用 allSettled:

js 复制代码
const results = await Promise.allSettled(files.map(upload));
const success = results
  .filter((r) => r.status === "fulfilled")
  .map((r) => r.value);
const failed = results
  .filter((r) => r.status === "rejected")
  .map((r) => r.reason);

坑在哪 :allSettled 不抛错,所有结果都要自己从 {status, value|reason} 里拆。批量场景首选它,别一上来就 Promise.all。


2. Promise.race:超时控制的灵魂

经典场景:fetch 偶尔挂死(网络问题),要给个兜底超时。

js 复制代码
function fetchWithTimeout(url, ms = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("timeout")), ms),
    ),
  ]);
}

坑在哪 :fetch 那个 Promise 还是会继续在飞------超时后内存里还挂着,连接也没真断。想真取消得用 AbortController(见 tip 9)。


3. Promise.any:找最快成功的那个

js 复制代码
const fastest = await Promise.any([fetchCDN1(), fetchCDN2(), fetchCDN3()]);
// 三个里谁先成功用谁的结果

ES2021 加的,比手写 race + try/catch 简洁不少。

坑在哪 :全部 reject 时抛 AggregateError,错误数组在 .errors 上,要专门 catch,否则报错信息一脸懵。


4. forEach 不支持 await 顺序

js 复制代码
// ❌ 反例:典型踩坑
files.forEach(async (file) => {
  await upload(file);
});
console.log("上传完成"); // ❌ 立刻打印,文件还在上传中

正解------for...of 串行:

js 复制代码
for (const file of files) {
  await upload(file);
}
console.log("上传完成"); // ✓ 真·上传完才打印

坑在哪 :forEach 把 async 回调当 fire-and-forget,return 的 Promise 全被丢了。需要并行的就 Promise.all,需要串行的用 for...of,别用 forEach + async。


5. 微观:串行 vs 并行的性能差距

10 个 1 秒的请求:

js 复制代码
// 串行:10 秒
for (const id of ids) await fetchUser(id);

// 并行:≈1 秒(瓶颈是网络)
const users = await Promise.all(ids.map(fetchUser));

坑在哪:并行不是越多越好,浏览器对同一域名并发有限(一般 6 个),多了排队。Node 服务端也别瞎并行,数据库连接池小的话直接打爆。


6. 手写并发池:限流核心

js 复制代码
async function pool(tasks, limit = 3) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const p = Promise.resolve().then(() => task());
    results.push(p);
    executing.add(p);

    const clean = () => executing.delete(p);
    p.then(clean, clean);

    if (executing.size >= limit) {
      await Promise.race(executing);
    }
  }
  return Promise.all(results);
}

用法:

js 复制代码
const tasks = urls.map((url) => () => fetch(url));
await pool(tasks, 3); // 同时最多 3 个

坑在哪 :手写池子容易死锁(比如 executing 没清理干净)。生产用 p-limit / async-pool 等库省心,自己写也就 30 行,但每个边界条件都得测一遍。


7. async/await 错误传播:for 循环里一个挂全挂

js 复制代码
// ❌ 反例:上传 5 个文件,第二个失败后面 3 个直接不跑
for (const file of files) {
  await upload(file);
}

正解------每个独立 try/catch:

js 复制代码
for (const file of files) {
  try {
    await upload(file);
    success.push(file);
  } catch (e) {
    failed.push({ file, error: e });
  }
}

坑在哪 :业务上是"部分失败容忍"还是"全成功才行"------前者写后者就丢数据,后者写前者又不够"宽容"。想清楚再写!想清楚再写!想清楚再写! ⚠️


8. 重试 + 指数退避

js 复制代码
async function retry(fn, retries = 3, base = 100) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === retries - 1) throw e;
      // 100, 200, 400, 800... 加点随机防雪崩
      const delay = base * 2 ** i + Math.random() * 100;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}

坑在哪 :重试幂等性!非幂等的请求(POST)重试会重复下单 ------必须带 idempotency key。必须带!必须带!必须带! ⚠️ 加点 jitter 随机延迟是防雪崩的关键。


9. 取消 fetch:AbortController 现代做法

js 复制代码
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch(url, { signal: controller.signal });
  clearTimeout(timer);
  // 用数据
} catch (e) {
  if (e.name === "AbortError") console.log("主动取消");
  else throw e;
}

坑在哪 :老浏览器不支持 AbortController(虽然 2026 年基本普及),要做 polyfill。Node 18+ 原生支持 signal。配合 tip 2 的超时模式一起用,才是真·超时取消。 【⚠️】请求就像泼出去的水,不能回撤。 取消后,请求会继续,但是会被忽略。


10. 顺序控制的"心智模型":并行 / 串行 / 池化

一句话速记:

  • 全并行 :Promise.all(tasks.map(t => t())) ------ 简单暴力,可能压垮服务
  • 全串行 :for (const t of tasks) await t() ------ 慢,但顺序可控
  • 池化限流 :pool(tasks, n) ------ 实战首选,平衡性能和资源

坑在哪 :业务场景没想清楚就拍脑袋选------比如写日志收集用全并行直接挂服务、用户操作链用全串行响应慢到被骂。用错模式背锅的是自己 😢


📦 收个尾

这 10 个技巧浓缩一下:

  • 最高频踩坑 :Promise.all 一挂全挂、forEach 不支持 await、try/catch 跨 setTimeout
  • 最值得收藏 :手写并发池、AbortController 真取消、Promise.allSettled
  • 核心思路 :并行 ≠ 全 Promise.all,按场景选模式

学到了就是赚到了,犹豫徘徊等于白来~


写到最后

想要啥技巧?评论区甩个题目过来~

  • 你刚踩的坑
  • 项目里反复写的代码
  • 想搞清楚但一直懒得查的 API

小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~

Ps:三连随缘,催更的会被打 😂

相关推荐
夏幻灵3 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
Lvan的前端笔记3 小时前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
三天不学习4 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs194054 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
明月_清风5 小时前
Deno 终局来了:从挑战 Node 到被 Cloudflare 收编
前端·后端·node.js
Csvn6 小时前
框架性能优化
前端
回眸&啤酒鸭8 小时前
【回眸】OpenSwarm 多智能体协作系统实战指南
大数据·前端·人工智能
用户69371750013848 小时前
2026,程序员的时代拐点到了
android·前端·后端
大龄秃头程序员8 小时前
一次 iBeacon + BLE 无感解锁方案的实现记录
前端
小兔子8 小时前
Python 的 GIL 与 free-threading:3.13 之后「去 GIL」走到哪一步了
前端