并发请求
前言
并发请求 在我们日常开发中可以作为一个请求优化的方案,也是一道 高频的面试题。
在有些时候,我们的站点上有一些请求可能会产生很多,比方说大文件上传中的切片上传,会把很多切块进行多次上传请求。或者说是做一些数据的抓取时,都会产生大量的请求。
当请求很多的时候,如果说我们等一个请求完成之后再进行下一个请求,那么这个效率就比较低了。
所以,我们可能会同时发出多个请求,但是又不能发的太多。因为我们的网络信道也是有资源限制的。
因此,我们就需要去控制这个同时发出请求的数量,也就是 并发数。
拟定并发请求方法
所以,我们可以写这么一个函数来控制并发请求:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls, maxNum) {
return new Promise((resolve) => {
// ...后续补充
})
}
我们可以给这个 concurRequest
方法传一个 url
地址的数组,再传一个最大并发数。当所有的请求全部完成之后返回一个 Promise
,当所有请求都完成之后这个 Promise
就完成。
这个 Promise
是 没有失败状态 的,哪怕你请求出错了,也是 resolve
,只是要把失败原因保存起来。
并发请求执行流程
我们请求的 最终结果 需要按照以下方式进行:
假设我们要发出的请求 urls
中,有 ABCDE
这 5
个请求地址,并且设置了最大并发数量为 3
。那么当所有请求完成之后,需要把请求的结果放到一个数组里边,并且这个 数组中存放的每一个请求结果的索引要和请求 url
一一对应。
那么具体的请求过程是怎么样的呢?
首先,按照最大并发数量,我们先抽出 3
个 url
进行请求,如果此时 B
先请求完成了,那么就先把 B
的请求结果存放到数组中对应的位置:
因为 B
完成了,此时 AC
还未完成,所以 D
可以来进行补位,充分地利用网络带宽,你不能说等 ABC
都完成之后才进行下一个,只要有一个结束了并且不满足最大并发数量,就得补位继续进行请求:
依次类推,直到所有的请求都完成,然后把请求结果(不管成功还是失败)全部放到结果数组里边。
这个结果数组就是我们整个 concurRequest
要返回的结果。
封装发送单个请求的函数
从上述的流程分析中,我们可以看到,实际上我们是 需要有一个函数来帮助我们发送其中某一个请求 的。
我们怎么知道要发哪个请求呢?这一块我们可以准备一个 下标。这个函数 要做的事情就是根据这个 下标 指向的 urls
数组中,取出对应的 url
来发送一个请求,然后把下标指向下一个,继续发送请求。
以此类推。
如果有这么一个函数的话,那么我们后边的处理就会方便很多,这里我们可以在 concurRequest
函数里边再定义一个 request
方法来发送单个请求:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls, maxNum) {
return new Promise((resolve, reject) => {
let index = 0 // 下一次请求对应的 url 地址的下标
const result = [] // 存放最终的请求结果
/**
* 根据下标指向的 url,发送单个请求
*/
async function request() {
const currentIndex = index // 保存当前请求的下标
// 取出下标指向的 url,然后 index++
const url = urls[index]
index++
// 请求有可能成功,也有可能失败,所以需要 try catch
try {
// 发送单个请求
const res = await fetch(url)
// 这里不能直接通过 result.push() 存放请求结果,因为我们不知道请求完成的顺序。
// result.push(res)
result[currentIndex] = res
} catch (error) {
// 请求失败也是一样,也需要将失败的结果存入 result 中
result[currentIndex] = error
}
}
})
}
处理到这里了还有一个核心问题没处理,就是当调用了这个 request
发送某个请求后,某个请求完成之后,还得应该把下一个请求拿出来执行,也就是 补位。
因此,我们可以在 try catch
中写一个 finally
:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls, maxNum) {
return new Promise((resolve, reject) => {
let index = 0 // 下一次请求对应的 url 地址的下标
const result = [] // 存放最终的请求结果
/**
* 根据下标指向的 url,发送单个请求
*/
async function request() {
const currentIndex = index // 保存当前请求的下标
// 取出下标指向的 url,然后 index++
const url = urls[index]
index++
// 请求有可能成功,也有可能失败,所以需要 try catch
try {
// 发送单个请求
const res = await fetch(url)
// 这里不能直接通过 result.push() 存放请求结果,因为我们不知道请求完成的顺序。
// result.push(res)
result[currentIndex] = res
} catch (error) {
// 请求失败也是一样,也需要将失败的结果存入 result 中
result[currentIndex] = error
} finally {
// 重新调用 request
if (index < urls.length) {
request()
}
}
}
})
}
好了,到现在就还剩一件重要的事情,就是 concurRequest
函数中返回的这个 Promise
什么时候完成呢?
有的人可能会认为这个下标越界了就完成了,这是不对的,因为下标越界了只能说明已经取出 urls
中所有的 url
来执行请求了,并不能说明所有请求都完成了。
所以,我们还需要借助一个变量 count
来记录请求完成的数量:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls, maxNum) {
return new Promise((resolve, reject) => {
let index = 0 // 下一次请求对应的 url 地址的下标
let count = 0 // 请求完成的数量
const result = [] // 存放最终的请求结果
/**
* 根据下标指向的 url,发送单个请求
*/
async function request() {
const currentIndex = index // 保存当前请求的下标
// 取出下标指向的 url,然后 index++
const url = urls[index]
index++
// 请求有可能成功,也有可能失败,所以需要 try catch
try {
// 发送单个请求
const res = await fetch(url)
// 这里不能直接通过 result.push() 存放请求结果,因为我们不知道请求完成的顺序。
// result.push(res)
result[currentIndex] = res
} catch (error) {
// 请求失败也是一样,也需要将失败的结果存入 result 中
result[currentIndex] = error
} finally {
count++ // 每次请求完成,count 计数加一
// 判断是否请求完成
if (count === urls.length) {
resolve(result)
}
// 重新调用 request
if (index < urls.length) {
request()
}
}
}
})
}
处理并发数量
最后就是来处理并发数量,这个并发数量其实影响的是什么?其实就是 request
需要调用几次。所以,只需要做一个循环处理就好了:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls, maxNum) {
return new Promise((resolve, reject) => {
let index = 0 // 下一次请求对应的 url 地址的下标
let count = 0 // 请求完成的数量
const result = [] // 存放最终的请求结果
/**
* 根据下标指向的 url,发送单个请求
*/
async function request() {
const currentIndex = index // 保存当前请求的下标
// 取出下标指向的 url,然后 index++
const url = urls[index]
index++
// 请求有可能成功,也有可能失败,所以需要 try catch
try {
// 发送单个请求
const res = await fetch(url)
// 这里不能直接通过 result.push() 存放请求结果,因为我们不知道请求完成的顺序。
// result.push(res)
result[currentIndex] = res
} catch (error) {
// 请求失败也是一样,也需要将失败的结果存入 result 中
result[currentIndex] = error
} finally {
count++ // 每次请求完成,count 计数加一
// 判断是否请求完成
if (count === urls.length) {
resolve(result)
}
// 重新调用 request
if (index < urls.length) {
request()
}
}
}
// 处理并发请求
// 这里需要注意一个问题,传入的 maxNum 有可能会超过 urls 的长度,所以这里需要判断一下
for (let i = 0; i < Math.min(urls.length, maxNum); i++) {
request()
}
})
}
细节优化
如果传入的 urls
长度为 0
,我们什么都不需要做,只需要 resolve
一个空数组就行了:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls = [], maxNum) {
return new Promise((resolve) => {
if (urls.length === 0) {
resolve([])
}
// ...省略
})
}
完整代码测试
模板页面 index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>并发请求</title>
</head>
<body>
<button>点击发送并发请求</button>
<!-- 引入并发请求方法 -->
<script src="./concurRequest.js"></script>
<!-- 主程序 -->
<script src="./index.js"></script>
</body>
</html>
并发请求 concurRequest.js
:
/**
* 并发请求方法
* @param { string[] } urls 待请求的 url 数组
* @param { Number } maxNum 最大并发数量
*/
function concurRequest(urls = [], maxNum) {
return new Promise((resolve) => {
if (urls.length === 0) {
resolve([])
}
let index = 0 // 下一次请求对应的 url 地址的下标
let count = 0 // 请求完成的数量
const result = [] // 存放最终的请求结果
/**
* 根据下标指向的 url,发送单个请求
*/
async function request() {
const currentIndex = index // 保存当前请求的下标
// 取出下标指向的 url,然后 index++
const url = urls[index]
index++
// 请求有可能成功,也有可能失败,所以需要 try catch
try {
// 发送单个请求
const res = await fetch(url)
// 这里不能直接通过 result.push() 存放请求结果,因为我们不知道请求完成的顺序。
// result.push(res)
result[currentIndex] = res
} catch (error) {
// 请求失败也是一样,也需要将失败的结果存入 result 中
result[currentIndex] = error
} finally {
count++ // 每次请求完成,count 计数加一
// 判断是否请求完成
if (count === urls.length) {
resolve(result)
}
// 重新调用 request
if (index < urls.length) {
request()
}
}
}
// 处理并发请求
// 这里需要注意一个问题,传入的 maxNum 有可能会超过 urls 的长度,所以这里需要判断一下
for (let i = 0; i < Math.min(urls.length, maxNum); i++) {
request()
}
})
}
主程序 index.js
:
const urls = []
for (let i = 1; i <= 200; i++) {
urls.push(`https://jsonplaceholder.typicode.com/todos/${i}`)
}
const btn = document.querySelector('button')
btn.onclick = async () => {
// 设置最大并发数量为 5
const res = await concurRequest(urls, 5)
console.log(res);
}
请求结果如下所示,请求以最大并发数量 5 个 url
来进行并发请求: