• Promise.all和请求池在实现并发控制网络请求时有什么异同?
  • 发布于 1个月前
  • 63 热度
    0 评论
有时候会遇到需要同时请求多个接口的场景,比如:获取学生列表,然后需要根据每个学生的id获取学生参加的社团的数量(意思是这么个意思,就是要依赖一个数组的每一项进行另一个请求获取其他数据)(听起来应该是后端将每个课程的人数一块返回了才正常),但是确实是有不正常的情景,即返回的学生信息不包含学生参与的社团数量,需要额外调用接口。假设有三四十个学生,那就意味着需要循环调用三四十个获取参与的社团数量的接口,显然对服务器造成的压力是很大的,显然不可以。此处就采用请求池的方法

使用Promise.all的方法:
Promise.all方法似乎是很容易想到的,将所有的Promise对象(即每个请求)都放入Promise.all中处理,等Promise.all处理结束后再往下执行
async const () => {
  const list = [xxx] // list 是课程数组
  const promiseList = list.map((item)=>{
    return axios.get(`https://xxx?id=${item.id}`)
  })

  await Promise.all(promiseList)
  // 执行后续操作
}
  // 堆代码 duidaima.com
  // 这里请求多个接口我用下列代码来模拟:
  const fn = () => {
    const arr = []
    for(let i = 1;i < 100;i++){
      arr.push(axios.get('/test/api' + i))
    }
    await Promise.all(arr)
    // 执行后续操作
  }
接着看一下每个接口所需的时间(由于接口是乱写的,所以报错很正常,主要是看一下请求时间):

如果是使用请求池,花费的时间如下:

可以看到请求所花费的时间减少了很多。

请求池
为什么是请求池?和Promise.all有什么区别?
Promise.all是等多个请求都响应后才能触发后续操作,请求池是上一个请求响应后就可以往队列继续添加而不需要等待其他请求。请求池的主体思想是利用队列进行模拟(先进先出,即先进先发送请求),当然,不一定非得使用队列这一数据结构,这里就利用数组来模拟。
import axios from 'axios'

export const handQueue = (
  list // 请求总数
) => {
  list = list || 0

  const requestQueue = (maxNum) => {
    maxNum = maxNum || 6 // 最大并发数
    const queue = [] // 请求池
    let current = 0 // 当前请求了多少条

    const dequeue = () => {
      while (current < maxNum && queue.length) {
        current++
        const currentPromise = queue.shift() // 出列
        currentPromise()
          .then(() => {
            // 成功的请求逻辑
            console.log('执行成功')
          })
          .catch((error) => {
            // 失败
            console.log(error)
          })
          .finally(() => {
            current--
            dequeue()
          })
      }
    }

    return (currentPromise) => {
      queue.push(currentPromise) // 入队
      dequeue()
    }
  }

  const enqueue = requestQueue(6)
  // console.log('enqueue:', enqueue)

  for (let i = 0; i < list; i++) {
    enqueue(() => axios.get('/api/test' + i))
  }
}
// 使用时在页面调用handQueue即可
关于请求池花费的时间可以上移回去看两张对比图。

整合到项目中
在请求池中,在拿出来队头那一个Promise对象之后,在then的回调函数中,肯定不是只局限与打印出接口成功返回的信息,往往需要结合实际进行其他操作。这里就讲一讲笔者踩的坑以及对应的解决方法:
约定:项目需求是在一个已经给定的数组中,遍历每一项,根据每一项的id调用接口获取其他信息

问题一:接口调用顺序不正常
前面说了,请求池是上一个请求响应后就执行下一个,前一个响应了并不能保证前一个接口调用就完成了(细细品一下这一句话)。每个接口的请求时间肯定是多多少少有区别的,所以并不能保证接口成功返回的顺序还是调用时的顺序。这一点也是可以得到验证的,如下:图片
可以发现接口返回的顺序并不是按正常的下标顺序执行的

问题二:并非任何时候都能拿到请求结果
任何时候一定能在请求池中拿到请求的结果吗?
答案是否定的
以笔者开发遇到的场景来说,我是在onLoad生命钩子中执行请求池函数的,并打印了执行结果:
const { res } = handQueue(list)
console.log(res)

由于异步是非阻塞的(在请求池中执行的就是多个网络请求),在执行打印的时候可能请求池中的请求都没来得及执行完,所以此时打印会出现拿不到值的情况。


笔者的解决方法比较暴力,直接加一个setTimeout,让其500ms之后再打印,惊奇的发现500ms完全够这么多个请求执行完毕,也就兴高采烈的完成任务啦
当然,这种方案不够优雅,毕竟今天请求池里执行四五十个接口500ms够用,万一明天请求的接口变成了一百多个500ms不够用咋办,总不能将计时器的倒计时时间设置大一点吧。不仅抽象,且影响用户体验,大佬们如果有其他方案,欢迎交流!
用户评论