🚦 手写 Promise 并发控制:如何优雅限制异步请求的并发数?

问题场景

需要一次性处理几百个请求/任务,如果直接全部 Promise.all,瞬间并发太多:

js 复制代码
// 100 个请求同时发出 → 服务器扛不住,甚至被限流/封 IP
const results = await Promise.all(
  urls.map(u => fetch(u))
)

浏览器对同一域名的并发连接本身有限制,但你自己发起的异步任务(HTTP 请求、爬虫、批量图片处理、API 限速)并不会被自动节流。结果就是:内存暴涨、服务器 429、超时重试满天飞。

你需要一个限制并发数的调度器:同时最多 N 个在跑,跑完一个补一个。

原因分析

Promise.all 是"一次全开",没有并发上限的概念,也没有让任务在"队列里等待"的机制。要实现并发控雨,本质是手动维护一个任务队列 + 运行中的计数:每当一个任务结束(无论成功失败),就从队列里弹出下一个去执行,始终保持同时进行中的任务数 ≤ N。

解决方案

方案 1:最简版 ------ p-limit 式思想(推荐理解)

js 复制代码
function createLimiter(limit) {
  const queue = [];        // 等待队列
  let active = 0;          // 进行中数量

  function next() {
    if (active >= limit || queue.length === 0) return;
    const { fn, resolve, reject } = queue.shift();
    active++;
    Promise.resolve()
      .then(fn)
      .then(resolve, reject)
      .finally(() => { active--; next(); });  // 结束,补下一个
  }

  return function enqueue(fn) {
    return new Promise((resolve, reject) => {
      queue.push({ fn, resolve, reject });
      next();
    });
  };
}

// 用法
const limit = createLimiter(3);   // 最多 3 个并发
const results = await Promise.all(
  urls.map(u => limit(() => fetch(u).then(r => r.json())))
);

关键点:

  • enqueue 返回新 Promise,把任务的 resolve/reject 存进队列,不立刻执行
  • next() 负责"名额有空就发车",任务完成(finally)后再拉下一个,形成滚动执行
  • 成功/失败都走 resolve/reject,且不影响队列继续跑(异常被捕获、不会拖垮整个批处理)。

方案 2:m 个一组 Promise.all 分波(简单但效率略低)

js 复制代码
async function chunkAll(items, limit, worker) {
  const results = [];
  for (let i = 0; i < items.length; i += limit) {
    const batch = items.slice(i, i + limit);
    results.push(...await Promise.all(batch.map(worker)));
  }
  return results;
}

缺点:前一批全部完成才开后一批,如果第一批里有个任务很慢,其余快的任务也得干等,整体吞吐不如滚动式。

方案 3:现成库

bash 复制代码
npm i p-limit   # 生态最广
js 复制代码
import pLimit from 'p-limit';
const limit = pLimit(3);
const results = await Promise.all(urls.map(u => limit(() => fetch(u))));

p-limit 内部实现和方案 1 基本相同,生产直接用这个,稳定可靠。

要点总结

  • Promise.all 无并发上限,批量任务必须手动限流,否则服务器 429 / 内存爆掉。
  • 核心是 任务队列 + 进行中计数 :结束一个(finally)就补下一个,保持并发数恒定 ≤ N。
  • 推荐滚动式 (跑完即补)而不是分波式(一批全完再下一批),后者会被慢任务拖累吞吐。
  • 任务无论成功失败都要 next(),别用 then 而漏了 catch 导致队列停摆------务必 finally
  • 生产项目直接上 p-limit,自写用于学习和理解原理。
  • 进阶:可再加重试 (失败入队重试)、超时Promise.race 兜底)、优先级(队列按权重排序)。

一句话:并发控制 = "同时只放 N 个在跑"的队列调度器。理解了 queue + active + next() 三个要素,你就吃透了所有并发库的底层逻辑。

相关推荐
gb42152871 小时前
python中Web应用服务器
开发语言·前端·python
默_笙1 小时前
💌 为了把"主题色"传给孙子组件,我翻了五层楼——直到遇见了 useContext
前端·javascript
颜进强1 小时前
04 - 一张图看懂 OpenSpec 变更的一生:从创建到归档
前端·后端·ai编程
the_answer2 小时前
JS 垃圾回收机制
前端
小KK_2 小时前
JS 事件循环从小白视角入门:宏任务、微任务与 async/await 一网打尽
前端·javascript
aloha_2 小时前
Linux服务器上指定目录的文件下载
前端
szp20053 小时前
为了在浏览器里跑多线程 ONNX 推理,我把自己的支付浮层弄挂了
前端·webassembly
kisshyshy3 小时前
从 useRef 到 Web Worker:理解 React 可变对象与浏览器多线程
前端·javascript·react.js
fatcoder3 小时前
玩转Nginx 04 — 反向代理:给 nginx 接上后端
前端·后端·nginx