本文详解如何使用JavaScript Promise实现异步并发任务控制器。从单线程与异步概念入手,逐步展示三种实现方案:基础循环控制、支持动态添加任务的类封装,以及基于Set优化的最终版本。通过代码解析与对比,帮助开发者掌握并发限制、动态任务添加及错误处理的最佳实践。
前言
实现一个控制并发数的任务队列或异步并发任务控制器,是前端开发中非常经典的手写面试题。由于其中涉及异步编程与并发控制的知识点,在正式编写代码前,有必要先厘清相关概念。理解其背后的原理,有助于更好地实现功能,而非仅仅死记硬背代码。

异步 & 并发
异步
单线程的 JavaScript
默认情况下,JavaScript 运行在单线程环境中,即所有代码仅在一个线程上执行。
需要注意的是,JavaScript 虽然表现为单线程运行,但这并不意味着 JavaScript 引擎仅有一个线程。实际上,引擎内部包含多个线程,单个脚本只能在主线程上运行,其他线程则在后台协助配合。
单线程特性意味着所有任务必须排队执行,只有当前任务结束后,后续任务才会开始。如果前置任务耗时较长,后续任务将被迫等待。
JavaScript 异步的产生
若排队是因为计算量过大导致 CPU 处理不过来,尚属合理。但在许多场景中,CPU 处于空闲状态,却因 IO 设备(如网络请求读取数据)响应缓慢,导致 CPU 必须等待结果返回才能继续执行后续代码。
JavaScript 语言的设计者意识到,当主线程无需等待 IO 设备时,可以挂起当前等待中的任务,先执行后续排队的任务。待 IO 设备返回结果后,再回过头继续执行被挂起的任务。
为更好地处理异步问题,JavaScript 中通常采用 Promise 或 async/await 机制。
并发
早期计算机 CPU 为单核架构,同一时刻只能执行一个进程或线程。当系统中有多个进程或线程等待执行时,CPU 必须依次执行完一个后再执行下一个。
所谓并发,是指在同一时刻只能有一条进程指令执行,但多个进程指令被快速交替执行。从宏观角度看,这表现为多个进程同时执行的效果;但从微观层面看,它们并非真正同时执行,而是将时间划分为若干片段,使多个进程快速交替运行。

实现异步并发任务控制器
基于上述对异步和并发概念的理解,现在开始具体实现并发任务控制器。
题目如下:
假设需要发送多个请求,但需实现并发控制。通过一个 limit 参数限制并发数,当任务数量超过 limit 限制时,后续任务需延迟执行,直到正在执行的任务完成后才能继续。同时,需支持动态添加额外的异步任务,并在最后一个任务执行完成后,执行对应的 callback 回调函数。
生成任务集合
// 生成用于测试的任务集合
const tasks = new Array(10).fill(0).map((v, i) => {
return function task() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(i + 1)
}, i * 1000);
})
}
})方式一:并发控制函数 concurrencyControl
核心思路
通过循环执行当前队列头部的任务。
当当前队列头部任务执行完毕后:
- 若为最后一个任务,则执行 callback。
- 否则,继续执行下一个队头任务。
// 并发控制函数
function concurrencyControl(tasks, limit, callback) {
const queue = tasks.slice() // 当前执行的任务队列
let count = 0 // 已完成的任务数量
const runTask = () => {
while (limit) {
limit--
if (queue.length) {
const task = queue.shift() // 取出当前队头任务
task().then(() => {
limit++
count++
if (count === tasks.length) { // 最后一个任务
callback() // 执行回调函数
}else{
runTask() // 继续执行下一个任务
}
})
}
}
}
return runTask
}
// 测试代码
const sendRequest = concurrencyControl(tasks, 3, (taskId) => {
console.log(`task ${taskId} finish!`)
})
sendRequest()不同时间的任务:

相同时间的任务:

方式二:并发控制器 ConcurrencyControl
方式一虽能简单实现自动化的并发控制,但不支持动态添加任务的要求,这意味着需要保持状态。此外,若当前执行的 promise 任务状态为 rejected,由于未提供 task().then 对应的 onreject 回调,将无法执行完全部任务。因此,我们可以通过一个 ConcurrencyControl 类来实现更完善的功能。
核心思路
- 将原本使用的变量转换为对应的实例属性。
- 新增 addTask() 方法用于动态添加任务,并在其内部自动启动任务执行。
- 将 task().then 替换为 task().finally,目的是当对应的 promise 任务为 reject 状态时,仍能继续执行后续任务。
class ConcurrencyControl {
constructor(tasks, limit, callback) {
this.queue = tasks.slice() // 当前执行的任务队列
this.tasks = tasks // 原始任务集合
this.count = 0 // 已完成的任务数量
this.limit = limit
this.callback = callback
}
runTask() {
while (this.limit) {
this.limit--
if (this.queue.length) {
const task = this.queue.shift() // 取出队头任务
task().finally(() => {
this.limit++
this.count++
if (this.count === this.tasks.length) { // 最后一个任务
this.callback() // 执行回调函数
} else {
this.runTask() // 继续执行下一个任务
}
})
}
}
}
addTask(task) {
// 同步添加任务
this.queue.push(task)
this.tasks.push(task)
// 当直接调用 addTask 也可直接执行
this.runTask()
}
}
// 测试代码
const Control = new ConcurrencyControl(tasks, 3, () => {
console.log(`task all finish!`)
})
// 执行队列任务
Control.runTask()
// 添加新任务
Control.addTask(function task() {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`task ${Control.tasks.length} finish!`)
resolve(Control.tasks.length)
}, Control.tasks.length * 200);
})
})方式三:优化 并发控制器 ConcurrencyControl
核心思路
- 优化掉 this.count 计数,通过 this.queue.size 来代替。
- 优化掉 this.addTask() 方法中的 this.queue.push(task),通过 this.tasks 的变化来自动影响 this.queue 队列。
- 优化掉 this.limit ++/--,通过 this.queue.size < this.limit 来替换。
class ConcurrencyControl {
constructor(tasks, limit, callback) {
this.tasks = tasks.slice() // 浅拷贝,避免修改原数据
this.queue = new Set() // 任务队列
this.limit = limit // 最大并发数
this.callback = callback // 回调
}
runTask() {
// 边界判断
if(this.tasks.length == 0) return
// 当任务队列有剩余,继续添加任务
while (this.queue.size < this.limit) {
const task = this.tasks.shift() // 取出队头任务
this.queue.add(task) // 往队列中添加当前执行的任务
task()
.finally(() => {
this.queue.delete(task) // 当前任务执行完毕,从队列中删除改任务
if (this.queue.size == 0) {
this.callback() // 执行回调函数
} else {
this.runTask() // 继续执行下一个任务
}
})
}
}
addTask(task) {
// 同步添加任务
this.tasks.push(task)
// 当直接调用 addTask 也可直接执行
this.runTask()
}
}
// 测试代码
const Control = new ConcurrencyControl(tasks, 3, () => {
console.log(`task all finish!`)
})
Control.runTask() // 执行队列任务
Control.addTask(function task() { // 添加新任务
return new Promise((resolve) => {
setTimeout(() => {
console.log(`task 9999 finish!`)
resolve(999)
}, 100);
})
})最后
以上就是本文的全部内容,通过以上三种实现方式的逐步优化,最终得到了一个比较合适的结果。当然,实现方式并非只有文中提到的这种,只需选择自己最容易理解的方式即可。
以上就是JavaScript Promise实现异步并发任务控制器的详细内容,更多关于JS Promise异步并发控制的资料请关注本站其它相关文章!