← 中文资源

简体中文版本

限制并发数的异步映射

以固定并发上限处理数组任务,保持结果顺序,支持快速失败与 AbortSignal 取消。

阅读英文原文

功能说明

mapWithConcurrency 把一个数组映射成异步结果,但同一时刻运行的任务数不会超过 limit。它适合批量请求、文件处理、调用外部接口等需要限制并发度的场景。结果按输入下标写入,因此任务完成的先后顺序不会改变返回数组的顺序。

默认采用快速失败策略:任意任务抛错后不再领取新任务,并在所有已启动的 worker 结束后抛出第一个错误。如果把 stopOnError 设为 false,函数会继续处理剩余元素,最终返回结果数组,调用方需要自行检查哪些位置可能没有值。传入 AbortSignal 可以在 worker 领取下一个任务前中止执行。

关键代码

启动的 worker 数量取 limit 与数组长度的较小值。每个 worker 循环领取递增下标,并在进入 mapper 前检查取消信号;异常被记录到 firstError,快速失败模式下直接退出循环。

const worker = async () => {
  while (true) {
    if (signal?.aborted) {
      throw signal.reason ?? new DOMException("Aborted", "AbortError");
    }
    if (failed && stopOnError) return;
    const index = nextIndex;
    nextIndex += 1;
    if (index >= items.length) return;
    try {
      results[index] = await mapper(items[index], index);
    } catch (error) {
      if (!failed) {
        failed = true;
        firstError = error;
      }
      if (stopOnError) return;
    }
  }
};

使用注意

取消信号只能阻止尚未开始的任务,已经进入 mapper 的异步操作必须自己监听 signal 并在内部停止,否则仍会消耗资源。快速失败模式下,Promise.all 会在第一个 worker 抛错时拒绝,其他 worker 可能仍在运行;如果希望等待全部结束,应使用 allSettled 或改为收集错误模式。

items 必须是真正的数组,类数组或可迭代对象需要先转换。limit 必须为正整数;若大于数组长度,实际 worker 数会降为数组长度。收集错误模式下结果数组可能包含未赋值的空洞,读取前要判断。mapper 抛出的值会原样抛出,不会被包装成新的错误类型。

验证方式

示例先以并发 2 处理 5 个睡眠时间递增的任务,输出 [1, 4, 9, 16, 25],并通过 order 数组观察 start 与 end 交错,确认同时运行数不超过 2。第二组任务在值为 2 时抛错,catch 应得到 failed on 2。把睡眠时间调大后运行,可以看到只有前两个任务开始,直到其中一个结束才会启动第三个。