问题场景
需要一次性处理几百个请求/任务,如果直接全部 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()三个要素,你就吃透了所有并发库的底层逻辑。