前端如何控制并发请求
什么情况需要前端控制并发请求,在需要多次才能请求完所需数据的时候。比如接口一次返回,数据很多,让浏览器渲染卡顿甚至崩溃,这时候我们可以分批同时发出6个请求,这样就可以避免卡顿或者崩溃。
那么前端如何控制并发请求呢?
前端控制并发请求的关键思路
比如有 20 个请求,需要按照 3
个一组,第一组请求完毕后再请求第二组,以此类推。
关键思路,把请求方法和请求参数使用一个数组存起来,然后每次请求3个,请求完毕后再请求下一个3个。每组请求返回后,把结果保存起来,等所有请求都返回后,再把所有结果返回。
api 设计
pControl
: 并发请求控制器, 传递最大并发数; add
: 添加请求和参数; start
: 开始请求,返回promise, 请求完毕后通过 .then
获取所有结果;
代码实现
function pControl(limit) {
const taskQueue = [] // {task: Function, params: any[]}[]
return {
add,
start
}
function add(task, params) {
taskQueue.push({
task,
params
})
}
function start() {
return runAllTasks()
}
function runAllTasks() {
const allResults = []
return new Promise((resolve) => {
runTask()
function runTask() {
if (taskQueue.length === 0) {
// 递归结束
return resolve(allResults)
}
const needRunSize = Math.min(taskQueue.length, limit)
const tasks = taskQueue.splice(0, needRunSize)
const promises = tasks.map(({
task,
params
}) => task(params))
Promise.all(promises).then((resList) => {
allResults.push(...resList)
// NOTE 递归调用的位置很关键
runTask()
})
}
})
}
}
关键代码解读
pControl
: 这个函数返回一个对象,包含add
和start
两个方法,add
用来添加任务和参数,start
用来开始请求,是一个闭包。runAllTasks
: 返回一个promise
,然后在new Promise
内部递归地执行runTask
, runTask 通过Promise.all
执行并发请求,在Promise.all().then()
再次调用runTask
,实现一组请求返回,再执行第二组请求。
实现分组等待的关键是在
.then
中递归调用。
思考: runAllTasks 可以使用循环实现吗?
能,需要使用 async 和 for 循环 + await
:
async function runAllTasks2() {
const allResults = []
const groupArr = []
let startIndex = 0
// 划分分组
while (startIndex < taskQueue.length) {
const arr = taskQueue.slice(startIndex, startIndex + limit)
groupArr.push(arr)
startIndex += limit
}
for (let index = 0; index < groupArr.length; index++) {
const pList = groupArr[index].map(({
task,
params
}) => task(params))
const res = await Promise.all(pList)
allResults.push(...res)
}
return allResults
}
在 for 中循环中不能使用
.then
,否则下一次循环不会等待上一次循环。
使用 for of
迭代实现:
async function runAllTasks2() {
const allResults = []
const groupArr = []
let startIndex = 0
// 划分分组
while (startIndex < taskQueue.length) {
const arr = taskQueue.slice(startIndex, startIndex + limit)
groupArr.push(arr)
startIndex += limit
}
// 迭代分组
const it = groupArr.entries()
for (const [key, value] of it) {
const pList = value.map(({
task,
params
}) => task(params))
const res = await Promise.all(pList)
allResults.push(...res)
}
return allResults
}
循环和 Promise 结合是怎样使用的呢?
for
、 while
、 for...of
等命令式循环结构,想要在循环中实现等待效果,必须使用 async
函数包裹循环中的 await
,不能使用 .then
。
forEach
、 map
、 filter
等函数式循环结构,不支持等待效果,因为这些函数式循环结构是同步的,不支持等待。
async
和循环
+await
结合,实现循环之间等待效果。
promise.then
和递归
结合,实现循环之间等待效果。
完善 api,让其更加易用
- 设置默认参数:给
pControl
设置一个合适的默认值,设置为6
,因为同一个域名在,浏览器的并发请求是 6 个。 - start给回调:通过回调能拿到每个分组的请求结果和知道当前完成的请求数量。
这两个改进很简单。先看用法:
const asyncTaskControl = pControl() // 默认 6
asyncTaskControl.add(task, params1)
asyncTaskControl.add(task, params2)
// ...
asyncTaskControl.add(task, params10)
asyncTaskControl.start((res, doneSize) => {
// 获取每组请求的结果 和当前完成了多少请求
console.log(res) // [{index:number,result:data}]
console.log(doneSize)
}).then(allResults => {
// 所有请求结果
console.log(allResults)
})
start 回调有什么作用呢?
方便使用者拿当前并发请求的结果,方便计算完成进度。
把上述功能封装成 p-control
npm 包发布
可通过 npm i p-control
下载使用。
小结
.then
和递归结合,实现异步任务之间等待;for
、while
等循环和async
+await
结合使用,实现异步任务之间等待;- 使用
Promise.all
实现多个异步任务并发执行。