标准答案

  1. 并发控制限制的是任务启动数量,不是限制已经创建好的 Promise。输入最好是一组返回 Promise 的函数,因为函数还没调用前任务没有开始。
  2. 实现时维护三个核心状态:下一个要启动的下标、当前运行中的任务数量、结果数组。只要运行数量小于限制,就继续启动新任务。
  3. 每个任务完成后,把结果写回原下标位置,运行数量减一,再补启动下一个任务。这样可以保证最多同时运行 N 个,并且最终结果顺序和输入顺序一致。
  4. 错误策略要提前定清楚:可以一个任务失败就整体 reject,也可以像 allSettled 一样记录每个任务的 fulfilled/rejected 状态。不同业务场景需要不同策略。

题目解析

这里关注真实工程异步控制,比如批量请求、图片上传、文件处理。

已经创建的 Promise 往往已经开始执行,把它们传给调度器时,并发已经发生了。因此并发控制通常接收任务函数,而不是 Promise 实例。

结果顺序是容易忽略的边界。任务完成顺序通常不等于输入顺序,如果直接 push 结果,调用方拿到的顺序会不稳定。用原下标写入 results 可以保留顺序。

并发控制和 Promise.all 的职责不同。Promise.all 只组合结果,不负责限制任务启动数量;并发限制需要额外的队列调度逻辑。

代码示例

下面实现会限制同时运行的任务数量,并按输入顺序返回结果。

JavaScript
function runWithLimit(tasks, limit) {
  return new Promise((resolve, reject) => {
    const results = []
    let nextIndex = 0
    let running = 0
    let finished = 0

    function launch() {
      if (finished === tasks.length) {
        resolve(results)
        return
      }

      while (running < limit && nextIndex < tasks.length) {
        const current = nextIndex++
        running += 1

        Promise.resolve()
          .then(() => tasks[current]())
          .then((result) => {
            results[current] = result
            finished += 1
            running -= 1
            launch()
          }, reject)
      }
    }

    launch()
  })
}

常见误区

  • 把一组已经启动的 Promise 传进去,误以为还能限制并发。
  • 只控制启动数量,不在任务结束后继续补位。
  • 结果按完成顺序返回,破坏调用方预期。

作者信息