JavaScript设计任务队列,控制请求最大并发数。
场景:前端页面中需要同时发送20个请求,但是服务端有限制,需要前端控制并发数,保证最多同时发送10个请求。
要求:
- 最多同时执行的任务数为10个。
- 当前任务执行完成后,释放队列空间,自动执行下一个任务。
- 所有任务添加到任务队列后,自动开始执行任务。
知识点:Event Loop/宏任务/微任务
事件队列:
JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,JS会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
宏任务与微任务
不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。
以下事件属于宏任务:
setInterval()setTimeout()以下事件属于微任务
Promise().resolve().then(new Promise中传入的函数是立刻执行的)new MutaionObserver()当当前执行栈执行完毕时, 每一次 loop 的时候会并且先执行"最前面"的宏任务, 然后执行当前 loop 下所有的微任务, 所有微任务完毕之后, 进入下一次 loop, 执行接下来的宏任务, 重复上述过程。
完整代码
function createTask(i) {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(i);
}, 2000);
});
};
}
class TaskQueue {
constructor() {
this.max = 10; //最大并发数
this.taskList = [] //用shift方法实现先进先出
setTimeout(() => { //这里初始化队列后自动执行,后续有新任务添加则需要手动执行。
this.run()
})
}
addTask(task) {
this.taskList.push(task);
}
run() {
const length = this.taskList.length;
if (!length) {
return;
}
const min = Math.min(length, this.max);// 控制并发数量
for (let i = 0; i < min; i++) {
this.max--; //开始占用一个任务的空间
const task = this.taskList.shift();
task().then(res => {
console.log(res);
}).catch(error => {
console.log(error);
}).finally(() => {
this.max++; //任务完成,释放空间
this.run();//自动进行下一个任务
})
}
}
}
const taskQueue = new TaskQueue();
for (let i = 0; i < 20; i++) {
const task = createTask(i);
taskQueue.addTask(task);//当所有任务添加到队列中后自动执行。
}
由于addTask是同步方法,在测试中,当所有addTask执行完毕后,会执行构造器中的setTimeout方法,也即自动执行run。
此时第一次执行run方法时,会将所有的run方法串联起来。此时for循环取任务的时候也是在当前Event loop中的同步任务,因此会执行完循环,也就是填充10个任务(模拟网速较快),当挂起的异步任务有完成,finally执行,释放任务空间,此时继续执行run方法填充任务,以此循环。
推荐阅读/参考资料
[Promise控制请求并发数] www.bilibili.com/video/BV1XZ… JS每日一题:如何设计一个任务队列?控制请求最大并发数
[Event Loop详解] zhuanlan.zhihu.com/p/33058983
[JS深入系列] github.com/mqyqingfeng…