实现批量请求数据,并控制请求并发数量,最后所有请求结束之后,执行callback回调函数
题目描述:
实现如下函数,可以批量请求数据,所有的url在urls数组里边,同时可以通过max参数控制并发的最大请求数量,当所有的请求结束之后,执行callback函数
function sendRequest(urls: string[], max:number,callback: () => void)
解析:
题目本身的算法思想不是很难,其实就是同时并发多个请求,当其中有一个请求结束之后,再重新发送一个请求,等到所有的请求都结束之后,把结果传入到callback函数里边执行就可以了。
既然该题目的实现逻辑很简单,那么问题就来了,可以自己去写写试试,看看自己能不能完整的实现。
如果不能实现,其实也没啥,关键是找到自己的问题在哪里?然后思考下在已知和结果之间的路自己为啥没有想到😁。
我当时思考的时候,就是卡在如何在一个请求结束之后,重新发起一个请求,同时把该请求的结果放入到结果数组中?
这个无限的不断的请求,应该很容易想到使用递归。
我的想法是首先发送多个请求,当其中一个请求结束之后,在Promise的then方法里边重新发起请求。
但是这里有个问题,就是我们不知道当前返回的结果是对应哪个url的结果,就像fetchSingleUrl注释的那样。
const allRequests = [
"http://jsonplaceholder.typicode.com/posts/1",
"http://jsonplaceholder.typicode.com/posts/2",
"http://jsonplaceholder.typicode.com/posts/3",
"http://jsonplaceholder.typicode.com/posts/4",
"http://jsonplaceholder.typicode.com/posts/5",
"http://jsonplaceholder.typicode.com/posts/6",
];
function sendRequest(urls,max,callback) {
const urlsLen = urls.length;
const promiseAll = [];
// 最后所有的结果
const res = new Array(urlsLen).fill(0);
let curIndex = -1;
function fetchSingleUrl(curRes) {
// 这里获取到的curIndex其实是错误的,无法确定当前返回的结果是该url获取的
console.log(999,curIndex)
res[curIndex] = curRes;
curIndex++;
if(curIndex >= urlsLen) {
callback(res);
}else {
fetch(urls[curIndex]).then(fetchSingleUrl).catch(fetchSingleUrl);
}
}
// 如果urls总共比max还小,那么就使用Promise.all一起发出去
if(urlsLen <= max) {
for(let i =0;i<urlsLen;i++) {
promiseAll.push(fetch(urls[i]));
}
Promise.all(promiseAll).then(curRes => {
console.log(112,curRes)
callback(curRes);
}).catch((err)=> {
console.log(333,err)
callback(err);
})
}else {
for(let i =0;i<max;i++) {
fetch(urls[i]).then(fetchSingleUrl).catch(fetchSingleUrl)
}
}
}
sendRequest(allRequests, 2, (result) => console.log(result));
上面的方法可以解决一部分问题,但是无法解决在返回的结果数组中,每个数组的结果是对应url的结果。
那有没有其它方法?
我们可以想下如果我们想让一个函数不执行,也就是等待状态改变的时候,我们在js中可以使用什么?
就是我们可以使用Promise,我们可以把一些这样的函数放入到一个队列中,当我们想要它运行的时候,我们取出来,然后把Promise的状态给改成已经完成状态,那么是不是就可以发送请求了。
最关键的是这段代码,这里会把超过最大请求数的请求,在这里使用Promise给holding住,同时把改变状态的resolve函数 push到一个数组中,一直等到有其它请求结束之后,从数组中拿出resolve函数,改变状态后,再执行下去
// 这个index传过来就是为了对应好哪个请求,
// 放在对应的results数组对应位置上的,保持顺序
async function request(index, reqUrl) {
// 阻塞队列增加一个 Pending 状态的 Promise,
// 这里最关键,就是如果当前发送的请求已经超过最大请求数了,在这里holding住,等待状态的改变后,再执行
if (currentReqNumbers >= max) {
await new Promise((resolve) => blockQueue.push(resolve));
}
reqHandler(index, reqUrl);
}
完整代码如下:
function sendRequest(urls, max, callbackFunc) {
const urlsLen = urls.length;
// 等待排队的那个队列
const blockQueue = [];
// 现在请求的数量,表示现在已经发送了几个请求
let currentReqNumbers = 0;
// 发送并且有返回的请求数量
let requestsDoneNumbers = 0;
// 存储最后的结果
const results = new Array(urlsLen).fill(0);
// 如果urls总共比max还小,那么就使用Promise.all一起发出去
if(urlsLen <= max) {
const promiseAll = [];
for(let i = 0;i<urlsLen;i++) {
promiseAll.push(fetch(urls[i]));
}
Promise.all(promiseAll).then(curRes => {
callbackFunc(curRes);
}).catch((err)=> {
callbackFunc(err);
})
}else {
init();
}
async function init() {
// 开始发送请求,如果超过最大max
for (let i = 0; i < urlsLen; i++) {
request(i, urls[i]);
}
}
// 这个index传过来就是为了对应好哪个请求,
// 放在对应的results数组对应位置上的,保持顺序
async function request(index, reqUrl) {
// 阻塞队列增加一个 Pending 状态的 Promise,
// 这里最关键,就是如果当前发送的请求已经超过最大请求数了,在这里holding住,等待状态的改变后,再执行
if (currentReqNumbers >= max) {
await new Promise((resolve) => blockQueue.push(resolve));
}
reqHandler(index, reqUrl);
}
async function reqHandler(index, reqUrl) {
currentReqNumbers++;
console.log("现在i是" + index + " 正请求:");
try {
const result = await fetch(reqUrl);
results[index] = result;
} catch (err) {
results[index] = err;
} finally {
currentReqNumbers--;
requestsDoneNumbers++;
console.log(`${index}请求结束`);
if (blockQueue.length>0) {
// 每完成一个就从阻塞队列里剔除一个
// 将最先进入阻塞队列的 Promise 从 Pending 变为 Fulfilled
blockQueue[0]();
blockQueue.shift();
console.log(
"消灭一个blockQueue第一个阻塞后,排队数为 : " + blockQueue.length
);
}
if (requestsDoneNumbers >= urlsLen) {
callbackFunc(results);
}
}
}
}
const allRequests = [
"http://jsonplaceholder.typicode.com/posts/1",
"http://jsonplaceholder.typicode.com/posts/2",
"http://jsonplaceholder.typicode.com/posts/3",
"http://jsonplaceholder.typicode.com/posts/4",
"http://jsonplaceholder.typicode.com/posts/5",
"http://jsonplaceholder.typicode.com/posts/6",
];
sendRequest(allRequests, 7, (result) => console.log(result));
总结:
在试用Promise的时候,可以把当前状态holding住,同时还可以把改变状态的函数给抛出去,等到合适的时机再执行
第二种思路
这里就是不使用Promise来把状态给hold住,我们当一个请求完成之后,我们把当前的发送请求的索引利用自执行函数同时传入进去,这样就能把返回的结果根据索引放入到结果数组中。同时根据是否已经执行过去发送下一个请求。
具体代码如下: 这里是typescript,可以先编译试试
function sendRequest(urls: Array<any>, max: number, callbackFunc: Function) {
const urlsLen: number = urls.length;
let finishedTask: number = 0;
let requestNumber: number = 0;
const res: Array<any> = [];
const visited: Array<boolean> = new Array(urlsLen).fill(false);
function run(start: number) {
console.log(finishedTask, urlsLen, start);
if (finishedTask >= urlsLen) {
callbackFunc(res);
return;
}
while (
requestNumber < max &&
requestNumber < urlsLen &&
start < urlsLen &&
finishedTask < urlsLen
) {
// eslint-disable-next-line no-loop-func
(function (i) {
requestNumber++;
start++;
visited[i] = true;
fetch(urls[i])
// eslint-disable-next-line no-loop-func
.then((curRes) => {
res[i] = curRes;
})
// eslint-disable-next-line no-loop-func
.finally(() => {
console.log(22222);
requestNumber--;
finishedTask++;
if (!res[i]) {
res[i] = "err11";
}
let flag: boolean = false;
for (let j = 0; j < visited.length; j++) {
if (!visited[j]) {
run(j);
flag = true;
break;
}
}
if (!flag) {
run(urlsLen);
}
});
})(start);
}
}
run(0);
}
const allRequests = [
"http://jsonplaceholder.typicode.com/posts/1",
"http://jsonplaceholder.typicode.com/posts/2",
"http://jsonplaceholder.typicode.com/posts/3",
"http://jsonplaceholder.typicode.com/posts/4",
"http://jsonplaceholder.typicode.com/posts/5",
"http://jsonplaceholder.typicode.com/posts/6",
];
sendRequest(allRequests, 2, (result) => console.log(result));