🚦 手写 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() 三个要素,你就吃透了所有并发库的底层逻辑。

相关推荐
宿6742 小时前
vue3-包管理器
前端·vue.js
@PHARAOH2 小时前
WHAT - Remix 入门和基本实践:理解两套产物
前端·remix
mayaairi3 小时前
Vue2 生命周期完全指南:从创建到销毁的8个钩子函数
前端·javascript·vue.js
xy34534 小时前
axure9.0 如何打造一个计时器(简单版)
前端·ui·html·axure·原型·产品设计
IMPYLH4 小时前
HTML 的 <pre> 元素
前端·javascript·html
tsumikistep5 小时前
【前端】Vue + Axios 跨域问题实战:7070 代理转发 8080 + 401 报错分析(黑马外卖)
前端·javascript·vue.js
lichenyang4535 小时前
鸿蒙首页从等高商品 Grid 到双列瀑布流:同一张图也能做出小红书式浏览节奏
前端
里欧跑得慢5 小时前
Flutter主题与样式详解
前端·css·flutter·web
计算机魔术师5 小时前
TPU推理性价比翻50%,NVIDIA的护城河要见底了?
前端
IT_陈寒6 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端