前言
前两天我的同事告诉我一个困扰着他的问题,就是低代码平台中存在很多模块,这些模块的渲染是由模块自身处理的,简言之就是组件请求了自己的数据,一个两个模块还好,要是一次请求了几十个模块,就会出现请求阻塞的问题,而且模块的请求都特别大。
大量的并发请求会导致网络拥塞和带宽限制。特别是当网络带宽有限时,同时发送大量请求可能会导致请求之间的竞争,从而导致请求的响应时间延长。
因此模块的加载就很不顺畅。。。
为了解决这个问题我设计了一个关于前端实现并发请求限制的方案,下面将详细解释这个并发请求限制的方案及实现源码。
核心思路及简易实现
一、收集需要并发的接口列表,并发数,接口调用函数。
二、遍历arr,对于每一项,创建一个promise实例存储到resArr中,创建的时候就已经开始执行了。
三、将创建的promise传入的实例数组中,对于每一项的promise设置其then操作,并将其存储到running数组中,作为执行中的标识。
四、当then操作触发之后则将running中的对应这一项删除,执行中的数组减一。
五、在遍历的回调函数最后判断当前是否超出阈值,当数量达到限制时开始批量执行,用await去处理异步,处理完一个即跳走,重新往running中注入新的实例。
async function asyncLimit(limitNum, arr, fn) {
let resArr = []; // 所有promise实例
let running = []; // 执行中的promise数组
for (const item of arr) {
const p = Promise.resolve(fn(item)); // 遍历arr,对于每一项,创建一个promise实例存储到resArr中,创建的时候就已经开始执行了
resArr.push(p);
if (arr.length >= limitNum) {
// 对于每一项设置其then操作,并将其存储到running数组中,作为执行中的标识,当then操作触发之后则将running中的对应这一项删除,执行中的数组减一
const e = p.then(() => running.splice(running.indexOf(e), 1));
running.push(e);
if (running.length >= limitNum) {
// 当数量达到限制时开始批量执行,处理完一个即跳走,重新往running中注入新的实例
await Promise.race(running);
}
}
}
return Promise.allSettled(resArr);
}
用例:
fn = (item) => {
return new Promise((resolve) => {
console.log("开始",item);
setTimeout(() => { console.log("结束", item);
resolve(item);
}, item) });
};
asyncLimit(2, [1000, 2000, 5000, 2000, 3000], fn)
注:但是这里的实现太过简陋,在真正的业务场景中往往没有这样使用场景,因此我对着段代码封装成一个符合前端使用的并发限制模块,下面是完整可用的代码实现
完整实现及源码用例
源码实现
首先,让我们来看一下代码及用例:
class AsyncExecutor {
constructor(limit, defaultFn) {
this.limitNum = limit;
this.defaultFn = defaultFn;
this.initTask()
}
asyncLimit() {
const task = this.promiseTaskQueue.shift();
if(task) {
const p = task()
const e = p.then(() => {
this.runningPromise.splice(this.runningPromise.indexOf(e), 1)
if(this.promiseTaskQueue.length === 0) {
this.initTask()
}
});
this.runningPromise.push(e);
if (this.runningPromise.length >= this.limitNum) {
this.racePromise = Promise.race(this.runningPromise); // 竞速promise
}
}
}
asyncExecute(item) {
this.promiseTaskQueue.push(this.getTask(item))
if (this.racePromise) {
this.racePromise = this.racePromise.then(async () => {
this.asyncLimit();
await Promise.race(this.runningPromise);
})
} else {
this.asyncLimit();
}
}
initTask() {
this.promiseTaskQueue = [] // 真实执行promise集合
this.runningPromise = []; // 执行中的promise集合
this.racePromise = ''; // 竞速promise,标志进入
}
getTask(item) {
return () => new Promise(resolve => {
resolve((item.fn || this.defaultFn)(item.value || item))
})
}
}
这里提供了一个面向对象的并发限制类。
测试用例
用例一:
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 3000 });
效果:
用例二:
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
setTimeout(()=>{
executor.asyncExecute({ value: 3000 });
}, 300)
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });
效果:
用例三:
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 3000 });
setTimeout(()=>{
executor.asyncExecute({ value: 1000 });
}, 300)
效果:
用例四:
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
executor.asyncExecute({ value: 2000 });
setTimeout(()=>{
executor.asyncExecute({ value: 1000 });
}, 300)
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 3000 });
效果:
用例五:
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
executor.asyncExecute({ value: 2000 });
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 });
setTimeout(()=>{
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 3000 });
}, 5000)
效果:
代码解释
这段代码是一个异步执行器的类(AsyncExecutor),用于控制并发执行异步任务。下面是对代码的逐行解释:
构造函数
constructor(limit, defaultFn):构造函数,接收并发限制数(limit)和默认处理函数(defaultFn)作为参数。this.limitNum = limit;:将并发限制数赋值给实例属性limitNum。this.defaultFn = defaultFn;:将默认处理函数赋值给实例属性defaultFn。- 调用
initTask初始化并发限制相关变量。
asyncLimit
asyncLimit 方法用于执行异步任务并控制并发数。它的主要作用是从任务队列中取出一个任务,并将任务转化为 Promise 实例进行执行。具体步骤如下:
- 从任务队列
promiseTaskQueue中取出第一个任务task。 - 如果存在任务,则执行以下操作:
- 执行任务
task并返回一个 Promise 实例p。 - 创建一个
完成后从正在执行的 Promise 数组中移除自身的 Promise 实例——e,并且当任务全部执行完毕之后会重新初始化并发限制相关变量,保证下次运行正常。 - 将 Promise 实例
e添加到正在执行的 Promise 数组runningPromise中,runningPromise表示正在执行中的Promise集合 - 如果正在执行的 Promise 数组长度达到并发限制数
limitNum,则创建一个竞速 PromiseracePromise,用于控制并发数。 racePromise是一个由Promise.race创建的promise,接收了runningPromise,runningPromise存储了调用完之后会删除自身的promise,此时标识当前AsyncExecutor进入了限制状态。
asyncExecute
asyncExecute 方法是业务侧用于调用并发控制的函数。它的主要作用是将任务添加到任务队列中,并根据当前是否存在竞速 Promise 进行相应的处理。具体步骤如下:
- 将任务函数
this.getTask(item)添加到任务队列promiseTaskQueue中,asyncLimit可以取出来执行。 - 如果存在竞速 Promise
racePromise,则执行以下操作:
- 使用
then方法等待竞速 Promise完成后执行下一步操作。 - 在竞速 Promise 完成后,意味着被限制的n个执行中的
promise已经出现执行结束的了,最大并发数中出现了一个空位。 - 在
竞速 Promise的then中重新调用this.asyncLimit()方法,从任务队列中取出新任务并执行,新的任务会加到runningPromise生成新的执行promise队列,所以后续所有的任务执行都得在新的执行promise队列执行完之后的then中执行。 - 下一步就要用
await去等待新的执行promise队列执行完毕,即:
await Promise.race(this.runningPromise); - 假如在队列达到了限制的情况下,又同时加入n个任务,该怎么保证每个任务都会在自己的
runningPromise之后的then中执行呢? - 每一次执行
asyncExecute即执行任务时,先判断当前是否存在racePromise,如果存在racePromise那么把执行函数放到的then中,并重置racePromise为设置then的racePromise,即:
this.racePromise = this.racePromise.then(async () => {
this.asyncLimit();
await Promise.race(this.runningPromise);
})
- 这样后续无论怎么调用,其任务执行函数都会在 上一个的
竞速 promise的 then 中执行,并且then通过await去等待新的竞速 promise的结束,这样就实现了并发控制。
- 如果不存在竞速 Promise,则直接调用
this.asyncLimit()方法执行异步限制。
getTask
根据传入的 item,返回一个获取任务的函数。
- 调用后,它会执行传入的处理函数或默认处理函数 并返回一个Promise。
initTask
初始化并发限制相关变量
this.promiseTaskQueue = []:存储执行中和执行过的 Promise 任务的数组。this.runningPromise = []:存储正在执行的 Promise 的数组。this.racePromise = '':标志进入竞速状态的 Promise,开始并发限制。
总结
使用上来说,需要先用AsyncExecutor类创建一个并发限制🚫的实例, 使用到接口的时候,调用实例的asyncExecute往 promiseTaskQueue 加数据就行,默认会直接执行 asyncLimit 并根据情况创建一个promise链。
并发限制的原理主要是通过阻塞 racePromise 即竞速 promise实现的,当并发达到限制数之后就创建一个 racePromise,后续的asyncLimit 任务执行都要在 racePromise的 then 中完成,当执行then时,会执行asyncLimit 任务,完成之后能得到新的racePromise,只有新的racePromise结束了这个then才能结束。
而后面的每一次的asyncExecute执行都会判断当前的 racePromise 是否存在 ,如果存在那么这次的执行就要放到 racePromise 的then中,并且将这个racePromise 赋值为 设置了then的新racePromise。这样就可以保证每一个任务执行都在上一个racePromise的then中,就形成一个promise链。
这样就可以实现并发请求限制🚫
结语
通过使用这个并发限制的机制,我们可以控制同时执行的异步操作数量,避免过多的请求对服务器造成压力,提高性能和用户体验。
文字的描述可能比较枯燥,读者可以去cv一下代码,并执行一下用例,看看效果并思考一下为什么会出现这种情况。
后续业务场景中如果遇到类似情况的话,可以有个解决思路。
后续:
新增返回 asyncExecute promise 能力
源码
class AsyncExecutor {
constructor(limit, defaultFn) {
this.limitNum = limit;
this.defaultFn = defaultFn;
this.initTask();
}
asyncLimit(resolve, reject) {
const task = this.promiseTaskQueue.shift();
if (task) {
const p = task();
const e = p.then((res) => {
resolve(res);
this.runningPromise.splice(this.runningPromise.indexOf(e), 1);
if (this.promiseTaskQueue.length === 0) {
this.initTask();
}
}).catch((err) => {
reject(err);
});
this.runningPromise.push(e);
if (this.runningPromise.length >= this.limitNum) {
this.racePromise = Promise.race(this.runningPromise); // 竞速promise
}
}
}
asyncExecute(item) {
// 返回新的pormise
return new Promise((resolve, reject) => {
this.promiseTaskQueue.push(this.getTask(item));
if (this.racePromise) {
this.racePromise = this.racePromise.then(async () => {
this.asyncLimit(resolve, reject);
await Promise.race(this.runningPromise);
});
} else {
this.asyncLimit(resolve, reject);
}
});
}
initTask() {
this.promiseTaskQueue = []; // 真实执行promise集合
this.runningPromise = []; // 执行中的promise集合
this.racePromise = ''; // 竞速promise,标志进入
}
getTask(item) {
return () =>
new Promise((resolve) => {
resolve((item.fn || this.defaultFn)(item.value || item));
});
}
}
用例
const executor = new AsyncExecutor(3, (item) => {
return new Promise((resolve) => {
console.log("开始", item);
setTimeout(() => {
console.log("结束", item);
resolve(item);
}, item);
});
});
executor.asyncExecute({ value: 1000 });
executor.asyncExecute({ value: 2000 }).then(res=>{console.log(2333, res, 'xiaoshan')});
executor.asyncExecute({ value: 5000 });
executor.asyncExecute({ value: 2000 }).then(res=>{console.log(2333, res, 'xiaoshan')});
executor.asyncExecute({ value: 3000 }).then(res=>{console.log(2333, res, 'xiaoshan')});;
解释
asyncExecute 函数返回一个新 Promise,这个promise会接收自身任务的执行结果。
后续就可以直接在executor.asyncExecute({ ... }) 中直接获取代码结果。