第一版设计与实现
实际需求
浏览器插件在A平台页面利用A页面接口发起近80个请求获取数据,类似爬虫功能,A页面对此接口做了访问限制,导致并发发起80个请求很容易出现请求失败,从根本上我们不能完全避免请求失败的情况发生,但我们可以通过降低并发请求量来尽可能的减少被限制的发生,因此我们需要做一个基于Promise请求队列的模块。
最初设计
-
将80个请求放入数组中,一批请求完毕wait一定时间继续执行下一批请求
-
根据第一点,我们需要设置这个队列每批请求的并发量和每批请求之间的wait时间
最初实现
基于以上思路,我们可以很快实现这个需求
// 模拟异步请求
function asyncTask() {
return new Promise(r => {
setTimeout(() => {
r(1)
}, 100);
})
}
const queue = []
// 生成30个异步请求,放到数组中
for(let i = 0; i < 30; i++) {
queue.push(asyncTask)
}
// 每批请求之间的等待函数
async function wait(time) {
return new Promise(r => {
setTimeout(() => {
r()
}, time);
})
}
以上是一些辅助函数和请求数据源的设置,接下来我们实现队列的核心模块
/**
* @param {*} max 每批请求最大量
* @param {*} interval 每批请求间隔等待时间
* @returns
*/
function requestQueue(max, interval) {
// 要返回一个promise,promise更改状态意味着整个队列执行完毕,暂时不考虑队列出错情况
return new Promise(r => {
const len = queue.length;
const result = [];
// 执行任务循环体
function executeTask(index = 0) {
const rest = len - index;
const executeArr = [];
// 为不影响数据源,使用executeArr来维护当前执行数据
for(let i = 0; i < (max > rest ? rest : max); i++) {
executeArr.push(queue[index + i]());
}
// 每批请求使用Promise.all
Promise.all(executeArr).then(async res => {
console.log(res);
result.push(...res);
index += (max > rest ? rest : max)
// index === len表明队列执行完毕,可以更改外层promise状态,若没处理完毕,则执行下一批请求
if(index === len) {
r(result)
}else {
// 等待一定时间
await wait(interval)
// 继续执行下一批请求
executeTask(index)
}
})
}
// 最初执行
executeTask()
})
}
简单说一下这里核心部分:
- requestQueue返回一个promise
- 明确队列是否执行完成的边界情况,这里我维护了一个index变量,当然还有别的实现方式
- 每批请求使用
Promise.all处理
至此,我们可以愉快的调用这个方法,同时也满足了我们最初的设计需求
// 每批请求4个并发量,每批请求间隔1000ms
requestQueue(4, 1000).then(res => {
console.log(res);
})
第二版设计与实现
实际需求
第一版实际上已经能够对付大多数的请求队列场景,但我们发现,每批请求中如果有一个请求响应非常慢,则会造成当前这批请求的阻塞,从而阻塞了整个队列。
改进设计
- 每批请求的颗粒度还是太大,我们将队列的颗粒度精确到每个请求,相当于每个请求独立,单个请求阻塞不会影响其他请求
我们通过两张图来看看现状和目标:
改进实现
// 改变模拟异步请求函数,为的是能让任务耗时不同
function asyncTask(time) {
return function() {
return new Promise(r => {
setTimeout(() => {
r(time)
}, time);
})
}
}
const queue = []
// 先放入耗时5000ms的异步请求
queue.push(asyncTask(5000))
// 生成5个耗时100ms异步请求,放到数组中
for(let i = 0; i < 5; i++) {
queue.push(asyncTask(100))
}
对辅助方法做了一些改变,接着再来改变核心模块
function requestQueue(max, interval) {
return new Promise(r => {
const len = queue.length;
const result = [];
let index = 0;
let hasFinishedCount = 0;
function run() {
let start = 0;
for(let i = 0; i < (max > len ? len : max); i++) {
executeTask(queue[start + i], start + i)
index += start + i
}
}
function executeTask(task, resultIndex) {
task().then(async res => {
console.log(res);
// 按照源数组位置放入结果数组中
result[resultIndex] = res;
hasFinishedCount++
// 判断队列是否完成
if(hasFinishedCount === len) {
r(result)
}else {
// 先检查一遍还有没有待执行的任务
if(index < len - 1) {
// 等待一段时间
await wait(interval);
// 等待一段时间后,还需要再检查一遍队列中有没有待执行的任务
if(index < len - 1) {
executeTask(queue[++index], index)
}
}
}
})
}
run()
})
}
-
接收的参数还是一样,但这里不再使用
Promise.all,每个请求独立执行,同时每个请求都能执行下一个请求 -
关键还是边界情况,维护了一个
hasFinishedCount变量
此时设置并发数为2,会发现第一个耗时为5000ms的任务并不会影响其他任务的执行,提高了整体的执行效率,可以和第一版做对比
// 每批请求2个并发量,每批请求间隔1000ms
requestQueue(2, 1000).then(res => {
console.log(res);
})
结束
到这里我们就完成了异步任务队列的核心模块,当然我个人封装的库在这基础上添加了许多功能:
- 引入了状态机制,队列可以暂停、继续以及清空
- 队列可以容错,即一个任务报错不会结束队列执行
- 可以设置任务优先级
- 可以设置每个任务的完成回调
- 更好的模块封装方便使用的api
如果有兴趣,可以去github看看,整体代码也只有200行。若对你有帮助,欢迎star!!!有任何问题,请提issue一起讨论