你知道怎么实现前端请求的并发控制和失败重试吗?

2,790 阅读2分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

在刷面经的时候,看见了一道要求实现请求的并发控制和失败重试的题目,刚好我自己的项目(一个小说爬虫,感兴趣可以看看)里也用到了这两个技术,于是在此整理一下实现的方法

并发控制

在某些场景中,前端需要在短时间内发送大量的网络请求,同时又不能占用太多的系统资源,这就要求对请求做并发控制了。这里的请求既可能是同一个接口,也可能是多个接口,一般还要等所有接口都返回后再做统一的处理。为了提高效率,我们希望一个请求完成时马上把位置空出来,接着发起新的请求。这里我们可以综合运用 Promise 的两个工具方法达到目的,分别是 raceall

在所有 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)

参考文章

前端API请求的各种骚操作