手动控制ajax请求最大并发数量,了解一下?

3,303 阅读3分钟

场景引入

现有100个异步请求需要发送,但由于某些原因,我们必须将同一时刻并发请求数量控制在10个以内,同时还要尽可能快速的拿到响应结果,应该怎样做?

不妨暂停阅读,思考一番,这是一个很有趣的问题。

思路剖析

我最先想到的是将请求分片,每片都在十个以内,然后依次Promise.all()。但很快发现,每组Promise.all之间存在等待时间,这并不是最快的方式。

最快的方式应该是一口气发十个请求,只要有一个请求响应成功,就立即发出下一个请求,循环往复,很明显这是一个递归的过程,我们应该在回调函数中做做文章。

具体实现

首先我们来实现这个递归函数:

function next(){
  // 处理递归到底的情况
  ···
  
  axios.get(url).then(res => {
    // 保存结果
    ···
    
    if (/* 请求完毕 */) {
      // 返回结果
      ···
    } else {
      next() // 递归
    }
  }).catch(/* 错误处理 */)
}

这样就可以实现接连调用,但我们还需要将数据按顺序收集起来,所以还需要一些额外的变量来保存状态。下面是一个完整的ajax函数:

const axios = require('axios')
const arr = new Array(100).fill('https://www.baidu.com')

/**
 * @param {Array<String>} arr 请求地址
 * @param {Number} n 控制并发数量
 */
function ajax (arr, n) {
  const { length } = arr
  const result = []
  let flag = 0 // 控制进度,表示当前位置
  let sum = 0 // 记录请求完成总数

  return new Promise((resolve, reject) => {
    // 先连续调用n次,就代表最大并发数量
    while (flag < n) {
      next()
    }
    function next(){
      const cur = flag++ // 利用闭包保存当前位置,以便结果能顺序存储
      if (cur >= length) return

      const url = arr[cur]
      axios.get(url).then(res => {
        result[cur] = cur // 保存结果。为了体现顺序,这里保存索引值
        if (++sum >= length) {
          resolve(result)
        } else {
          next()
        }
      }).catch(reject)
    }
  })
}

ajax(arr, 10).then(res => {
    console.log(res)
})

如此便可实现并发数量的控制,运行结果如下:

[
   0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,
  12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
  24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
  36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
  48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
  72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
  84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
  96, 97, 98, 99
]

思路延伸

除了并发请求,该问题还可以延伸到很多场景中。例如文件读取,假如有100个文件,一般不会一次性读入。

错误记录

中途我想到了滑动窗口,于是写出了下面的代码:

const axios = require('axios')
const arr = new Array(100).fill('https://www.baidu.com')
function ajax (arr) {
  let cur = 0 // 当前位置
  let size = 0 // 窗口大小
  const result = []
  while(arr.length !== 0) {
    if (size > 10) continue
    size++
    const url = arr.shift()
    axios.get(url).then(res => {
      result[cur] = cur
      size--
      cur++
    })
  }
  return result
}
ajax(arr)

且不说这段代码是否正确。我的思路是不断地循环,检测窗口大小,确定是否发出请求。下面说这段代码的问题。

JavaScript是基于事件循环的,每一轮事件循环都会先执行完宏任务再清空微任务。Promise.then是微任务,这意味着只有本轮宏任务结束才会执行。

这段代码中,while执行完前十次后,将会一致触发continue。原因是while循环是宏任务,还没有执行完毕,导致then方法注册的微任务没有机会执行,所以size变量无法减小,造成死循环。

这里记录一下错误的思路,也希望大家引以为戒。

如果还有其他实现方式,欢迎评论区留言讨论