引子
想象一下排队打疫苗,
外面排了一堆人,接种点里面就那么几个可以打疫苗的地方,上图里面得有13个可以同时打疫苗的位置,诊室里面打完一个人,出来一个人,外面排队的人,排在最前面的再进去一个。
好了我们类比一下这道题,我们知道Chrome浏览器同时可以进行6个并行的请求任务,这里要让我们自定义最多可以同时进行多少请求,也就是请求的并发度, 那也就是说,如果现在给了我们20个请求,20个url需要去fetch,同时定义了,最多同时可以进行3个请求,那么一开始,我们先从头开始fetch, 第1个到到第3个,进行的很顺利,从第4个开始,因为并行数最多是3, 从第四个开始阻塞住了,也就是需要开始排队了,等前面3个请求,其中有任意一个结束了之后,第四个也就是排在阻塞队列最前面才能够接着进行请求。
解题思路
给我们一个url的数组,里面比如说有20个url,什么都不考虑,就是不考虑最多并发数和阻塞的情况下,我们直接for loop url的数组,然后一个个fetch就行了,
但是如果我们要引入最多并发数呢?
打疫苗我们知道要排队,因为接种点里面打疫苗的窗口就那么多,那么这里,我们怎么给请求fetch 排队呢?
这里我们需要借助一下Promis,
案例一:
new Promise((resolve, reject) => {
resolve("success"); // 下面then 能够进行的前提是 我能够成功的执行resolve函数,
// 如果我执行reject函数,那么我们就会执行后面的catch部分内的函数
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
案例二:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success"); // 这里3000ms也就是3s之后, 成功的resolve之后,
// 下面then部分内的代码才能够正常的执行,否则就永远也不会执行到then里面的代码,
}, 3000);
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
这两个案例一结合, 这不就是我们想要的排队功能嘛,每个都给我Promise 上,然后只要我不给你resolve,你就在那侯着,等我什么时候给你resolve,什么时候轮到你发送请求。
那么我们让请求可以等候了,但怎么排队呢?那就是借助一个数组,我们模仿下堆(Heap):队列优先,先进先出。 也就是如果当我们的请求从前到后,如果这时候比如说进行到第3个请求了,但是我们最多并发数就是3, 那么在进行到第四个请求的时候,我们就判断一下,现在已经进行的请求数是否超过最多并发数MAX值,超过了,那么我们就把 先前讲到的那个概念,新建一个Promise, 然后把里面的resolve函数 push进 我们创建的等待队列(数组)里面,后面的请求都同样处理,如果超过了,那么就放队列里面去,这里我们也是按顺序走的哈,
然后,我们再把注意力重新放回来最开始的几个请求,也就是没有超过最多并发数时候的,假设我们一个请求完成了,之后我们要查看下,(是不是后面还有人等着打疫苗呢?是的话,我们就叫一下,让他过来打疫苗)这里翻译过来就是我们的等候队列里面是不是还有没成功执行的resolve, 是的话,我们就执行下resolve函数,让那个进程的可以继续向下进行也就是发送请求,然后再队列shift一下,最前头的去掉,因为执行完了嘛,就别占位置了, 最后最后,再看下我们总共发送请求的数量,因为我们每次查看也都是在request执行完了之后采取判断查看,所以这时候我们再检查一下,已经发送了的请求数量,是否等于人家最开始给我们的url数组的长度,也就是里面url的数量,如果相等,那么证明我们请求都执行完了,我们此刻可以运行callback 函数了。
以上就是我简述的一个逻辑,具体的还要看下代码,下面上代码!
代码
实际代码
const allRequest = [
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=4",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=5",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=6",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=7",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=8",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=9",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=10",
];
function sendRequest(urls, max, callbackFunc) {
const REQUEST_MAX = max;
const TOTAL_REQUESTS_NUM = urls.length;
const blockQueue = []; // 等待排队的那个队列
let currentReqNumber = 0; // 现在请求的数量是
let numberOfRequestsDone = 0; // 已经请求完毕的数量是
const results = new Array(TOTAL_REQUESTS_NUM).fill(false); // 所有请求的返回结果,先初始化上
async function init() {
for (let i = 0; i < urls.length; i++) {
request(i, urls[i]);
}
}
async function request(index, reqUrl) {
// 这个index传过来就是为了对应好哪个请求,
// 放在对应的results数组对应位置上的,保持顺序
if (currentReqNumber >= REQUEST_MAX) {
await new Promise((resolve) => blockQueue.push(resolve)); // 阻塞队列增加一个 Pending 状态的 Promise,
// 进里面排队去吧,不放你出来,不resolve你,你就别想进行下面的请求
}
reqHandler(index, reqUrl); // {4}
}
async function reqHandler(index, reqUrl) {
currentReqNumber++; // {5}
try {
const result = await fetch(reqUrl);
results[index] = result;
} catch (err) {
results[index] = err;
} finally {
currentReqNumber--;
numberOfRequestsDone++;
if (blockQueue.length) {
// 每完成一个就从阻塞队列里剔除一个
blockQueue[0](); // 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled,
// 也就是执行resolve函数了,后面不就能继续进行了嘛
blockQueue.shift();
}
if (numberOfRequestsDone === TOTAL_REQUESTS_NUM) {
callbackFunc(results);
}
}
}
init();
}
sendRequest(allRequests, 2, (result) => console.log(result));
调试代码(比实际多了几个log()而已)
const allRequest = [
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=4",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=5",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=6",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=7",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=8",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=9",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=10",
];
function sendRequest(urls, max, callbackFunc) {
const REQUEST_MAX = max;
const TOTAL_REQUESTS_NUM = urls.length;
const blockQueue = []; // 等待排队的那个队列
let currentReqNumber = 0; // 现在请求的数量是
let numberOfRequestsDone = 0; // 已经请求完毕的数量是
const results = new Array(TOTAL_REQUESTS_NUM).fill(false); // 所有请求的返回结果,先初始化上
async function init() {
// {1}
for (let i = 0; i < urls.length; i++) {
console.log("现在i是: " + i + " 正请求:" + urls[i]);
request(i, urls[i]);
}
}
async function request(index, reqUrl) { // 这个index传过来就是为了对应好哪个请求,
// 放在对应的results数组对应位置上的,保持顺序
// {2}
if (currentReqNumber >= REQUEST_MAX) {
console.log(
"currentReqNumber: " + currentReqNumber +
" ----REQUEST_MAX : " + REQUEST_MAX +
" ---- url: " + reqUrl);
// {3}
await new Promise((resolve) => blockQueue.push(resolve)); // 阻塞队列增加一个 Pending 状态的 Promise,
// 进里面排队去吧,不放你出来,不resolve你,你就别想进行下面的请求
console.log("第"+ i +"个请求等待结束: 即将开始执行:" + reqUrl);
}
reqHandler(index, reqUrl); // {4}
}
async function reqHandler(index, reqUrl) {
currentReqNumber++; // {5}
try {
const result = await fetch(reqUrl);
results[index] = result;
} catch (err) {
results[index] = err;
} finally {
currentReqNumber--;
numberOfRequestsDone++;
console.log(
"done request: " +
reqUrl +
" and currentReqNumber: " +
currentReqNumber +
" .blockQueue.length => " +
blockQueue.length
);
if (blockQueue.length) {
// 每完成一个就从阻塞队列里剔除一个
blockQueue[0](); // 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled
blockQueue.shift();
console.log(
"消灭一个blockQueue第一个阻塞后,排队数为 : " + blockQueue.length
);
}
if (numberOfRequestsDone === TOTAL_REQUESTS_NUM) {
callbackFunc(results);
}
}
}
init();
}
sendRequest(allRequests, 2, (result) => console.log(result));
到此终于完事了,网上关于这道题的答案鱼龙混杂,错误的一堆,正确的没几个,所以在此特地加上自己的一丁点改写,传播正确的答案写法,以免错误的答案传播,并且浪费大家的时间,希望能帮到各位看到这的读者们!
如果您觉得写的还不错有帮到你,还望赏个赞,多谢啦! 如果写的不好的地方还望评论区指正,感谢!
最后非常感谢参考资料中第二个的那篇文章的分享者,其分享的文章中提到了原作者;
那个分享自微信公众号 - Nodejs技术栈(NodejsRoadmap),作者:五月君
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
参考资料:
[1] JS请求并发数控制以及重发 题目图片粘贴自此
[2] 实现浏览器中的最大请求并发数控制