标准答案

  1. 并发控制接收的最好是任务函数数组,而不是已经创建好的 Promise 数组。
  2. 维护 nextIndex、running、finished 和 results。
  3. 只要 running 小于 limit,就继续启动下一个任务。
  4. 每个任务成功后按原始 index 写入结果,失败策略可以选择立即 reject 或记录错误后继续。
  5. 任务完成或失败后都要释放运行名额,否则队列会卡住。

题目解析

限制并发常见于批量上传、批量下载、批量接口补数据。它的目的不是让代码更复杂,而是避免瞬间打满浏览器连接数、后端限流或用户网络。

传入任务函数很重要。如果外部已经把 Promise 创建好了,请求可能已经全部开始,此时再限制只是在限制等待结果,不是在限制请求启动。

错误策略要提前讲清。手写题里可以采用快速失败;业务里也可能需要继续跑完剩余任务,再返回成功和失败明细。

代码示例

下面的实现限制最多同时运行 limit 个任务,并按输入顺序返回结果。

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

    if (limit < 1) {
      reject(new RangeError('limit must be greater than 0'))
      return
    }

    if (tasks.length === 0) {
      resolve([])
      return
    }

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

        Promise.resolve()
          .then(() => tasks[currentIndex]())
          .then(value => {
            results[currentIndex] = value
            finished += 1
            running -= 1

            if (finished === tasks.length) {
              resolve(results)
            } else {
              schedule()
            }
          }, reject)
      }
    }

    schedule()
  })
}

常见误区

  • 传入已经开始执行的 Promise,再说自己做了并发限制。
  • 任务完成后没有 running -= 1,导致后续任务永远不会启动。
  • 按完成顺序 push 结果,调用方无法对应原始任务。

作者信息