面试官:
请手写一个并发请求控制的函数/类,传入一个请求列表和最大并发数,这个函数可以控制请求的并发数,又可以实现并发,最后返回所有的请求结果
怎么用
先看结果,这个类怎么用
- 在构建类的实例对象的时候传入最大并发数
- 然后调用类的promiseAll方法去执行我们的请求列表,其余的表现形式等同于Promise.all
const plimit = new PLimit(3);
plimit
.promiseAll([
() => asyncFun("aaa", 2000),
() => asyncFun("bb", 3000),
() => asyncFun("ccc", 1000),
() => asyncFun("ccc", 1000),
() => asyncFun("ccc", 1000),
])
.then((res) => {
console.log(res);
});
解析
首先构建一个类,用属性count来存储最大并发数,用queue控制并发调度的队列
class PLimit {
count = 6; // 最大并发数
readonly queue: Function[] = []; // 调度队列
constructor(count: number) {
// 数据初始化
this.count = count;
}
}
prmiseAll
然后在类里面写一个prmiseAll方法,这个是我们请求的入口函数。
- 如果是以前直接使用原生的Promise.all的时候我们直接传入reqList给promiseAll就好,但是异步请求的状态改变我们没办法控制
- 但是由于我们要实现并发数控制,所以我们要在请求外面再包裹上一层promise,我们可以自己手动控制这一层的promise什么时候resolve
private async enqueue(fn: Function, resolve: Function) {
// 因为后面有对象, 所以保存本对象的this指向方便后面用
// 入队,格式为含有fn和index的对象
this.queue.push(this.run.bind(this, fn, resolve));
// 判断是否达到最大并发数,没有的话继续出队执行函数
if (this.activeCount < this.count && this.queue.length > 0) {
const func = this.queue.shift();
func && func();
}
}
enqueue
然后是入队函数**enqueue**
通过bind将请求函数fn等参数绑定在run函数()上,然后push到队列里面,
然后判断当前并发数,如果小于最大并发数的话,就可以继续执行队列里的请求函数
private async enqueue(fn: Function, resolve: Function) {
this.queue.push(this.run.bind(this, fn, resolve));
if (this.activeCount < this.count && this.queue.length > 0) {
const func = this.queue.shift();
func && func();
}
}
run
然后是执行方法run
- 请求执行前activeCount++,执行结束activeCount—,并且把结果resolve给外层的promise,结束掉这一个请求
- 然后继续出队,执行(有没有发现,其实本质是 队列调度+递归)
由于await不能捕捉错误,我本来还在外面包裹了try catch,后来一想如果请求出错,没有catch的错误会继续向外面抛出,最后会被原生的Promise.all包裹住的,所以不用担心
private async run(fn: Function, resolve: Function) {
this.activeCount++;
const res = await fn();
resolve(res);
this.activeCount--;
if (this.queue.length > 0) {
const func = this.queue.shift();
func && func();
}
}
全部代码:
最后是全部的代码,加了点注释和细节
// 其实就是本质就是利用队列去调度异步请求的执行,
// 利用promise.all去获取所有请求的执行结果
class PLimit {
count = 6; // 最大并发数
activeCount = 0; //当前并发数
readonly queue: Function[] = []; // 调度队列
constructor(count: number) {
if (!((Number.isInteger(count) || count === Infinity) && count > 0)) {
throw new TypeError(
"Expected `concurrency` to be a number from 1 and up"
);
}
// 数据初始化
this.count = count;
}
async promiseAll(reqList: Function[]) {
// 使用promise.all的时候,自己用一个promise去包装整个(入队enqueue,执行run,然后await到结果的过程),
// 等待到结果之后,调用resolve就能resolve掉这个promise
return Promise.all(
reqList.map(
(fn, i) =>
new Promise((resolve, reject) => this.enqueue(fn, resolve, reject))
)
);
}
/**
* @description: 具体执行请求的函数
* @param {number} index
* @return {*}
*/
private async run(fn: Function, resolve: Function, reject: Function) {
this.activeCount++;
try {
const res = await fn();
resolve(res);
} catch (e) {
reject(e);
}
this.activeCount--;
if (this.queue.length > 0) {
const func = this.queue.shift();
func && func();
}
}
/**
* @description: 入队函数
* @param {Function} fn 异步请求函数
* @param {number} index 异步请求函数对应的index
* @param {array} rest 剩余参数
* @return {*}
*/
private async enqueue(fn: Function, resolve: Function, reject: Function) {
// 因为后面有对象, 所以保存本对象的this指向方便后面用
// 入队,格式为含有fn和index的对象
this.queue.push(this.run.bind(this, fn, resolve, reject));
// 判断是否达到最大并发数,没有的话继续出队执行函数
if (this.activeCount < this.count && this.queue.length > 0) {
const func = this.queue.shift();
func && func();
}
}
}
function asyncFun(value, delay) {
return new Promise((resolve) => {
console.log("start " + value);
setTimeout(() => resolve(value), delay);
});
}
const plimit = new PLimit(2);
const res = async () => {
console.log("====================================");
console.log(
await plimit.promiseAll([
() => asyncFun("aaa", 2000),
() => asyncFun("bb", 3000),
() => asyncFun("ccc", 1000),
() => asyncFun("ccc", 1000),
() => asyncFun("ccc", 1000),
])
);
console.log("====================================");
};
res();
还有很多细节问题我可能没有注意到,欢迎在评论区留言指出~
参考: