这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
在刷面经的时候,看见了一道要求实现请求的并发控制和失败重试的题目,刚好我自己的项目(一个小说爬虫,感兴趣可以看看)里也用到了这两个技术,于是在此整理一下实现的方法
并发控制
在某些场景中,前端需要在短时间内发送大量的网络请求,同时又不能占用太多的系统资源,这就要求对请求做并发控制了。这里的请求既可能是同一个接口,也可能是多个接口,一般还要等所有接口都返回后再做统一的处理。为了提高效率,我们希望一个请求完成时马上把位置空出来,接着发起新的请求。这里我们可以综合运用 Promise 的两个工具方法达到目的,分别是 race 和 all。
在所有 promise 都没有 reject 的情况下:
Promise.all 会在所有的 promise 都 resolve 后返回包含所有 promise resolve 的结果的数组
Promise.race 会返回最快 resolve 的 promise 的值
代码实现如下:
/**
*
* @param {number} limit 并发限制数
* @param {(() => Promise<any>)[]} requests 包含所有请求的数组
* @returns {Promise<any[]>} 结果数组
*/
async function concurrentControl(limit, requests) {
// 存储所有的异步任务
const res = []
// 存储正在执行的异步任务
const executing = []
for (const request of requests) {
const p = Promise.resolve().then(() => request())
// 保存新的异步任务
res.push(p)
// 当limit值小于或等于总任务个数时,进行并发控制
if (limit <= requests.length) {
// 当任务完成后,从正在执行的任务数组中移除已完成的任务
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
// 保存正在执行的异步任务
executing.push(e)
if (executing.length >= limit) {
// 等待较快的任务执行完成
await Promise.race(executing)
}
}
}
return Promise.all(res)
}
简单解析一下流程,
首先遍历所有的 requests,加入 res 中,当limit <= requests.length,limit 值小于或等于总任务个数时,进行并发控制
这里有一段需要理解一下的代码
p.then(() => executing.splice(executing.indexOf(e), 1))
其实等同于下面这样
const e = p.then(fn);
executing.push(e);
// p resolve 后执行 fn
() => executing.splice(executing.indexOf(e), 1)
接下来当正在执行的请求大于 limit时,就需要调用await Promise.race(executing)等待一个请求执行完成,最后用Promise.all(res)返回所有结果
测试代码
let i = 0
function generateRequest() {
const j = ++i
return function request() {
return new Promise(resolve => {
console.log(`r${j}...`)
setTimeout(() => {
resolve(`r${j}`)
}, 1000 * j)
})
}
}
const requests = new Array(4).fill('').map(() => generateRequest())
async function main() {
const results = await concurrentControl(2, requests)
console.log(results)
}
main()
请求重试
在一些需要保证请求成功的场景下,我们还可能需要请求重试重试的功能,比如我项目中的请求章节失败,需要重新请求
接下来我们来看请求重试的实现
请求重试的实现相比起来并发控制就简单多了
async function retry(request, limit, times = 1) {
try {
const value = await request()
console.log('获取成功')
return value
} catch (err) {
if (times > limit) {
return err
}
console.log(`请求失败,第 ${times} 次重试...`)
return retry(request, limit, ++times)
}
}
只需要在捕获到错误的时候,递归调用retry函数就可以了,当超过最大重试次数时,就返回错误
测试代码
let i = 0
function generateRequest() {
const j = ++i
return function request() {
return new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? reject(`err`) : resolve('success')
}, 1000 * j)
})
}
}
async function retry(request, limit, times = 1) {
try {
const value = await request()
console.log('获取成功')
return value
} catch (err) {
if (times > limit) {
return err
}
console.log(`请求失败,第 ${times} 次重试...`)
return retry(request, limit, ++times)
}
}
retry(generateRequest(), 3).then(console.log)