场景引入
现有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
变量无法减小,造成死循环。
这里记录一下错误的思路,也希望大家引以为戒。
如果还有其他实现方式,欢迎评论区留言讨论。