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:三连随缘,催更的会被打 😂

相关推荐
Sinclair2 小时前
安企CMS的安装-PHPStudy(小皮面板)部署
前端·后端
会飞的特洛伊2 小时前
IAM身份认证
前端·后端
钛态2 小时前
AI 组件生成评测:别只看页面能不能渲染
前端·vue·react·web
Hyyy2 小时前
requestAnimationFrame和setTimeout有什么不同
前端
Jackson__3 小时前
问到你想清楚,这个 skill 专治没想明白就写代码!
前端·github·ai编程
এ慕ོ冬℘゜3 小时前
前端实战:Textarea 实时字数计数输入器(带字数限制+UI优化完整源码)
前端·ui
程序员黑豆4 小时前
从零开始:创建第一个鸿蒙应用程序
前端·harmonyos
只与明月听4 小时前
LangChain 学习-掌握LangChain Core API
前端·人工智能·后端
Listen·Rain4 小时前
Vue3中组件间通信详解
前端·javascript·vue.js