场景描述
- 笔者的一个朋友,最近出去面试,被问到了如何控制并发请求
- 笔者的朋友回答,浏览器自带并发控制为6,一般不需要控制
- 包括其他的浏览器,一般并发数都是6(一次发6个,第7个等前6个发完再发-队列思想)
- 面试官对笔者朋友的回答差强人意
- 于是乎,本文就探讨一下js并发请求的控制
为何谷歌浏览器控制并发数是6?
- 问:为何谷歌浏览器控制并发数是6?
- 答:这是浏览器设计的规则
- 问:从哪里能看到这个规则
- 答:浏览器源码中能看到
谷歌浏览器源码下载
- 笔者下载了谷歌浏览器的源码
- github地址如下:https://github.com/chromium/chromium
控制并发数6的谷歌浏览器源码
- 经过很长时间查找,总算找到了为何并发数是6的原因
- 如下截图:
大家感兴趣的话,也可以自行下载谷歌浏览器源码,去按照上图中文件位置,查看源码
- 笔者认为,我们在写代码中,一般不用刻意控制并发请求的数量,因为浏览器帮我们做了兜底的控制
- 比如大文件的分片上传操作,就是并发的多个请求
- 浏览器兜底并发6个,如上图示例
- 但是有的面试官还是希望我们能够进一步说说这个
- 甚至是能够手写一个函数,用于控制并发请求
接下来,我们继续说一下,js中写一个控制并发请求的函数...
笔者提供后端请求接口
- 以往的文章案例中,常常是代码不全(只有前端,没后端)
- 不太好做到复制粘贴即用演示,对于读者略微不太友好
- 为了方便大家更好的演示理解,笔者用自己的服务器,简单写了一个请求接口
简单接口
route.get('/getIdName', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
let key = req.query.id
res.send({
code: '0',
message: '成功',
data: {
idName: '这个ID名字为: ' + key
}
})
})
请求示例
let urls = [
'http://ashuai.work/api/getIdName?id=1',
'http://ashuai.work/api/getIdName?id=2',
'http://ashuai.work/api/getIdName?id=3',
'http://ashuai.work/api/getIdName?id=4',
'http://ashuai.work/api/getIdName?id=5',
'http://ashuai.work/api/getIdName?id=6',
'http://ashuai.work/api/getIdName?id=7',
'http://ashuai.work/api/getIdName?id=8',
'http://ashuai.work/api/getIdName?id=9',
'http://ashuai.work/api/getIdName?id=10',
'http://ashuai.work/api/getIdName?id=11',
'http://ashuai.work/api/getIdName?id=12',
'http://ashuai.work/api/getIdName?id=13',
'http://ashuai.work/api/getIdName?id=14',
'http://ashuai.work/api/getIdName?id=15',
'http://ashuai.work/api/getIdName?id=16',
'http://ashuai.work/api/getIdName?id=17',
'http://ashuai.work/api/getIdName?id=18',
];
返回结果示例
get请求,直接url赋值地址栏粘贴回车即可,如:http://ashuai.work/api/getIdName?id=6
{"code":"0","message":"成功","data":{"idName":"这个ID名字为: 6"}}
并发请求控制,方案一:直接使用Promise.all
Promise.all并发请求代码
复制粘贴即用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>循环异步请求</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>
<body>
<script>
let urls = [
'http://ashuai.work/api/getIdName?id=1',
'http://ashuai.work/api/getIdName?id=2',
'http://ashuai.work/api/getIdName?id=3',
'http://ashuai.work/api/getIdName?id=4',
'http://ashuai.work/api/getIdName?id=5',
'http://ashuai.work/api/getIdName?id=6',
'http://ashuai.work/api/getIdName?id=7',
'http://ashuai.work/api/getIdName?id=8',
'http://ashuai.work/api/getIdName?id=9',
'http://ashuai.work/api/getIdName?id=10',
'http://ashuai.work/api/getIdName?id=11',
'http://ashuai.work/api/getIdName?id=12',
'http://ashuai.work/api/getIdName?id=13',
'http://ashuai.work/api/getIdName?id=14',
'http://ashuai.work/api/getIdName?id=15',
'http://ashuai.work/api/getIdName?id=16',
'http://ashuai.work/api/getIdName?id=17',
'http://ashuai.work/api/getIdName?id=18',
];
Promise.all(urls.map(url => axios.get(url)))
.then(responses => {
let data = responses.map(response => response.data);
console.log(data);
})
.catch(error => {
console.error('报错啦: ', error);
});
</script>
</body>
</html>
效果图
- Promise.all丢出去一堆请求
- 它不关注,谁快谁慢
- 所以这种方式并发请求,也是可以的,不过唯一的确定就是不保证按照顺序走的
- 当然,就算是手写并发请求,也不一定完全保证请求顺序,除非并发请求设置单次执行(即一次并发1个请求)
一般来说,一些需求,前人都考虑到了,都会提供对应的npm包给我们使用,所以这里我们使用社区比较知名的并发请求的包:p-limit
什么是p-limit?
官方:Run multiple promise-returning & async functions with limited concurrency,Works in Node.js and browsers.
大白话:能够在node或者浏览器中使用的,控制并发的异步函数
是很优秀的一个npm包,周下载量过亿!
npm地址:https://www.npmjs.com/package/p-limit
使用p-limit进行并发控制
- 冗余的代码语法规则,这里不赘述
- 直接贴上完整代码(带注释)
- 老规则,复制粘贴即用
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>
<body>
<script type="module">
// 通过js模块方式在jsdelivr这个cdn中引入p-limit
import pLimit from 'https://cdn.jsdelivr.net/npm/p-limit@5.0.0/+esm'
const limit = pLimit(2); // 并发限制2个2个的发请求
// 一堆请求数组,用Promise.all接收
const input = [
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=1')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=2')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=3')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=4')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=5')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=6')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=7')
return res.data
}),
limit(async () => {
let res = await axios.get('http://ashuai.work/api/getIdName?id=8')
return res.data
}),
];
const result = await Promise.all(input);
console.log(result);
</script>
</body>
</html>
效果图
由下图可见,的确是两个两个的发请求
使用循环优化一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>
<body>
<script type="module">
// 通过js模块方式在jsdelivr这个cdn中引入p-limit
import pLimit from 'https://cdn.jsdelivr.net/npm/p-limit@5.0.0/+esm'
// 笔者提供的请求接口
let urls = [
'http://ashuai.work/api/getIdName?id=1',
'http://ashuai.work/api/getIdName?id=2',
'http://ashuai.work/api/getIdName?id=3',
'http://ashuai.work/api/getIdName?id=4',
'http://ashuai.work/api/getIdName?id=5',
'http://ashuai.work/api/getIdName?id=6',
'http://ashuai.work/api/getIdName?id=7',
'http://ashuai.work/api/getIdName?id=8',
'http://ashuai.work/api/getIdName?id=9',
'http://ashuai.work/api/getIdName?id=10',
'http://ashuai.work/api/getIdName?id=11',
'http://ashuai.work/api/getIdName?id=12',
'http://ashuai.work/api/getIdName?id=13',
'http://ashuai.work/api/getIdName?id=14',
'http://ashuai.work/api/getIdName?id=15',
'http://ashuai.work/api/getIdName?id=16',
'http://ashuai.work/api/getIdName?id=17',
'http://ashuai.work/api/getIdName?id=18',
]
const limit = pLimit(2);
const input = []
urls.forEach((url) => {
let item = limit(async () => {
let res = await axios.get(url)
return res.data
})
input.push(item)
})
const result = await Promise.all(input);
console.log(result);
</script>
</body>
</html>
补充 p-queue
- p-queue是p-limit的升级版
- 没错,是同一个作者呢
- 能够实现更为复杂的控制
- 一般来说,p-limit就够用了
- 附github地址:https://github.com/sindresorhus/p-queue
还是不少star的
- 实际上,作为调包侠的我们,若是空闲了,还是要研究一下一些npm包的原理的
- 接下来,我们手写一个队列
- 面试的过程中,把这个答出来
- 这个问题才算是回答的不错
自己手写一个并发请求函数
思路分析
- 因为是要等到拿到所有的请求,所以要用到Promise的语法
- 比如设置并发3个请求,那我们需要统计当前在跑的请求有没有达到3个
- 没达到就继续执行请求(从任务数组中取出一项执行),达到了先缓一缓(while循环更加合适)
- 当请求任务数组结束,并且也没有在跑的任务时
- 说明活全部干完了
- 再把结果resolve出去给外边
大致是这个思路,请看代码更加明晰
代码附上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.0/axios.js"></script>
</head>
<body>
<script>
function concurrentReq(urls, limit) {
return new Promise((resolve, reject) => {
let num = 0; // 当前在跑的请求数量(在跑的请求数量要小于限制的数量)
let task = urls; // 并发任务数组
let results = []; // 最终并发请求结果存放的数组
// 递归闭包函数调用发请求,Promise返回最终结果
function goNext() {
if (task.length === 0 && num === 0 ) {
// 当没有更多任务且没有请求正在进行时
resolve(results); // 所有请求已完成,resolve吐出去返回结果
return;
}
while (num < limit && task.length > 0) { // 当请求任务小于3且还有任务继续干时,goNext
let url = task.shift(); // 把并发任务数组中第一项剔除掉,并拿到第一项(请求接口)
num = num + 1 // 并记录一下当前的请求数量
axios.get(url) // 发请求
.then((res) => {
num = num - 1 // 请求成功,就把计数减一
results.push(res.data); // 把请求的结果依次存起来
goNext(); // 递归调用,继续执行下一个请求任务
})
.catch((err) => {
num = num - 1 // 请求失败,也把计数减一
console.error(`此接口:${url}请求失败,报错信息:${err}`);
goNext(); // 递归调用,继续执行下一个请求任务
})
}
}
goNext();
});
}
// 笔者提供的请求接口
let urls = [
'http://ashuai.work/api/getIdName?id=1',
'http://ashuai.work/api/getIdName?id=2',
'http://ashuai.work/api/getIdName?id=3',
'http://ashuai.work/api/getIdName?id=4',
'http://ashuai.work/api/getIdName?id=5',
'http://ashuai.work/api/getIdName?id=6',
'http://ashuai.work/api/getIdName?id=7',
'http://ashuai.work/api/getIdName?id=8',
'http://ashuai.work/api/getIdName?id=9',
'http://ashuai.work/api/getIdName?id=10',
'http://ashuai.work/api/getIdName?id=11',
'http://ashuai.work/api/getIdName?id=12',
'http://ashuai.work/api/getIdName?id=13',
'http://ashuai.work/api/getIdName?id=14',
'http://ashuai.work/api/getIdName?id=15',
'http://ashuai.work/api/getIdName?id=16',
'http://ashuai.work/api/getIdName?id=17',
'http://ashuai.work/api/getIdName?id=18',
]
let limit = 3; // 假设控制并发请求数量每次发三个
concurrentReq(urls, limit)
.then((results) => {
console.log('所有请求执行结果:', results);
})
.catch((error) => {
console.error('请求执行出错:', error);
});
</script>
</body>
</html>
效果图
总结
- 关于面试题如何控制js并发一百个请求?这个问题
- 我们可以回答到几个关键字
- 浏览器自带并发控制6个
- 市面上一些npm包也可以直接拿来用
- 当然自己也可以手写一个并发请求控制函数
- 这样回答这道面试题,面试官应该会满意的
要是你这样回答,面试官依旧不满意,呃,那你就当我没说😅😅😅
A good memory is better than a bad pen. Write it down...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。