功能说明
该示例解决直接使用 Promise.all 时无法限制并发的问题。mapPool 先校验数组、映射函数与并发度,然后启动固定数量的 worker,每个 worker 循环领取下一个索引,调用 mapper 并把结果写回同一下标。无论哪个任务先完成,返回数组的顺序都与输入一致,适合批量请求接口、处理文件列表或构建索引等需要控制资源占用的场景。
关键代码
共享的 nextIndex 在每次 await 之前同步自增,因此多个 worker 不会拿到同一个下标;这是 JavaScript 单线程事件循环下实现工作窃取的关键。worker 数量取并发度与数组长度的较小值,空数组不会启动任何任务。Promise.all 只等待 worker 循环结束,不直接等待业务任务,因此任一 mapper 抛错都会让整个池拒绝,其余已启动任务仍会继续执行到结束。
使用注意
该实现采用快速失败语义,若需要收集每个任务的成功与失败,应改为 allSettled 或在 mapper 内部捕获错误。并发度只限制同时在执行的任务数,不限制排队任务的内存占用,超大数组应改用流式分批。任务抛错后池不会自动取消其他任务,调用方需自行使用 AbortSignal 协调。返回值是全新数组,不会修改输入。
验证方式
用可控的延迟任务记录 active 计数,断言任意时刻 active 不超过设定并发度;用不同延迟打乱完成顺序,验证结果仍按输入下标排列;测试空数组返回空数组、并发度为零或小数抛 RangeError、mapper 非函数抛 TypeError,以及某个任务拒绝时整体 Promise 拒绝。
代码示例
export function mapPool(items, mapper, concurrency = 4) {
if (!Array.isArray(items)) throw new TypeError("items must be an array");
if (typeof mapper !== "function") throw new TypeError("mapper must be a function");
if (!Number.isInteger(concurrency) || concurrency < 1) {
throw new RangeError("concurrency must be a positive integer");
}
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const index = nextIndex;
nextIndex += 1;
results[index] = await mapper(items[index], index);
}
}
const workerCount = Math.min(concurrency, items.length);
const workers = Array.from({ length: workerCount }, () => worker());
return Promise.all(workers).then(() => results);
}