前言
我们需要知道,在 http1.1 的版本中 Chrome 浏览器对于同域名下的并发请求限制在 6 个,并且这样的限制不能修改,而是在源码里写死的。那么问题来了,如果同时发起多个请求,浏览器会如何处理呢?另外,为什么 http1.1 会有这样的情况? http2.0 是不是也有同样的问题呢?
其实,在 http 的不同版本过程中,一直在优化接口的传输可靠性和速度的及时性。 http 是建立在 tcp 之上的协议,我们没有很好的办法优化 tcp,只能想办法优化自己。这里不过多说这个,下面的思考都基于 http1.1 的基础上,http 2.0 有多路复用,不会有 6 个 tcp 链接的情况,是复用一个 tcp 连接。
本文的目录结构如下:
Chrome浏览器对于多个请求的处理情况
在这里,我们模拟了 17 个接口几乎同时请求的情况,如下图:
上图可以发现,浏览器会几乎同时发起这些请求,全部处于 pending 状态。
但是从上面这张图我们又可以发现,接口真的开始请求响应之后,第六个接口之后的请求会被阻塞。直到前面的请求被完成的时候,才会依次执行后续的接口。前面有很长一段的等待时间。
这似乎是一个非常好的解决大量接口请求,可能导致的 tcp 链接过多或者服务器扛不住的情况。
但是这里也会遇到一个问题: 如果实际的接口响应时间很慢,同时请求大量接口,对于处于后面pending的接口而言,有没有超时的风险呢?
其实肯定是有的,那么我们还有什么可以优化的方法吗?
我的想法是,既然浏览器可以控制接口的阻塞,使其一直等待。那我们也可以控制接口的请求数量,我们在最大限制量内,分批次处理请求,请求结束后,再去请求后续的接口。
我们从下面的几个方案来大概讲解一下,遇见这种情况,前端如何更好的处理。
我们先创建一个含有多请求 url 的数组,和最大并发量的限制变量,作为后面代码的前提条件。
const pics = [
'https://****/mock/**/getListForOpeCallPage',
'https://****/mock/**/listPage',
'https://****/mock/**/getCsCallList',
'https://****/mock/**/getPhoneLog',
'https://****/mock/**/getPhoneResult',
'https://****/mock/**/listInboundInstanceById',
'https://****/mock/**/inboundInstanceInfo',
'https://****/mock/**/getHidePhone',
'https://****/mock/**/listAllStaff',
'https://****/mock/**/selectBindCompanyListByRobotDefId',
'https://****/mock/**/getResultListByCall',
'https://****/mock/**/updateResult',
'https://****/mock/**/getInboundInstanceResult',
'https://****/mock/**/listInboundInstanceLog',
'https://****/mock/**/getResultList',
'https://****/mock/**/virtual',
'https://****/mock/**/actual',
];
const maxLoad = 5;
简单的 http 控制
http 的请求限制很简单,做到以下三个点即可:
- 我们需要一个 maxLoad 变量,控制实时发起请求的数量。这里临时定为 5。
- 还需要一个临时的下标,用于当某个接口完成的时候处理哪一个未请求的接口。
- 在 http 的 onload 回调里,递归调用请求函数。
代码如下:
function getUrlByHttp() {
let idx = maxLoad;
function getContention(index) {
console.log('我在执行', index);
const conn = new XMLHttpRequest();
conn.open('get', pics[index]);
conn.onload = () => {
console.log('当前是哪个id先返回', index);
idx++;
if(idx < pics.length){
getContention(idx);
}
};
conn.send();
}
function start() {
for (let i = 0; i < maxLoad; i++) {
getContention(i);
}
}
start();
}
我们一起看一下结果,如下图:当点击按钮的时候,会同时发起 5 个请求。并不会把所有的接口全部都发出。而这5个限制是由变量控制的,我们可以按情况更改最大并发的量。
那么当请求到后期,我们可以看见下图的加载顺序,在前面的 5 个接口请求中,任意一个接口结束后,就会马上发起下一个请求。在任意的时间段,请求数量都一定小于等于最大并发量:5。
关于上面的代码,我们还可以升级一下代码,使用更简便的 api 来实现。
关于 fetch 控制
虽然是http方法的升级版本,但其实和http一样的方法,只是减少了代码量,更干净而已。
function getUrlByFetch() {
let idx = maxLoad;
function getContention(index) {
fetch(pics[index]).then(() => {
idx++;
if(idx < pics.length){
getContention(idx);
}
});
}
function start() {
for (let i = 0; i < maxLoad; i++) {
getContention(i);
}
}
start();
}
这里可以注意,下一次执行不是绑定在 onload 的方法上,而是在有返回值的时候。
接口的最后完成时间会更短,因为不用等待资源下载的时间,如下图所示:
fetch 方法,其实也并不能完全满足目前项目的需求和编码习惯,我们可以尝试更高级的用法 - promise。
关于 promise 的实现
这里实现的功能和上面的需求不太一样,这里的需求是:现有大量的接口,使用 pomise 执行这些接口,保证每个时间的最大请求数在 5 个,并在接口全部成功或者失败的时候返回结果。
这个需求有 3 个重点:
-
在接口全部成功或者失败的时候,告知成功或者失败。
此时,我们肯定会首先想到使用 promise.all,这个api会返回请求数组全部请求成功或者失败的结果。
-
但是我们如何对请求的数组进行最大并发量的限制呢?
如果最大限制为 5,那么 promise.all 的数组长度只能是 5。
-
在长度被限制的情况下,我们如果保真所有的接口被一个接一个请求呢?
我们可以使用 Promise.race 监控当前 Promise.all 的接口数组的请求情况,返回第一个请求成功的接口。
具体代码如下:
function getUrlByPromise() {
/**
* 一个执行异步逻辑的promise函数,返回成功的异步id,或者失败的id
*/
function getFetch(url, idx) {
return new Promise(async (resolve, reject) => {
console.log(`发起第${idx}个请求`);
const res = await fetch(url);
if (!res) {
return reject();
}
return resolve();
});
}
function limitLoad() {
// 限制请求数量的数组,idx是第几个位置,用于验证是第几个位置的接口请求成功,需要更换接口
const promises = pics.slice(0, maxLoad).map((it, idx) => {
// 这里返回结束的idx,是限制数组的下标
return getFetch(it, idx).then(() => idx);
});
// 这里的reduce返回一个Promise.resolve()的promise,是包含了里面所有的feach请求回调注册完成的
return (
pics
.reduce((pre, cur, index) => {
if (index < maxLoad) {
return pre;
}
return (
pre
// 这里的对未来的请求的注册,先给每一个item注册这样的函数
// 当回调被执行的时候,就是某个位置的请求完成,并且返回位置的下标
.then(() => Promise.race(promises))
.catch((err) => console.log(err))
.then((idx) => {
// 第几个位置的请求结束,就重新放入一个请求,这个请求是当前的下标,并且返回当前的位置
console.log(
`第${idx}个位置的请求结束,将第${index}个接口放入,共${pics.length}个请求`
);
promises[idx] = getFetch(cur, index).then(() => idx);
})
);
}, Promise.resolve())
// promise.all控制并发数
.then(() => Promise.all(promises))
);
}
// 开始函数
function start() {
limitLoad()
.then((res) => {
console.log('资源全部加载成功', res);
})
.catch((rej) => {
console.log('资源加载失败', rej);
});
}
start();
}
我们一起来看一下结果:也是非常完美的串行+并行执行的效果。
我们再来看看响应的log输出,注意每一个log的输出顺序,第几个被执行,第几个请求结束,所有请求执行结束。
关于使用promise的控制并发量的方法,网上已经有很多现成的demo和库。这里demo的实现是完全自己的思路去完成的,大家可以多多参考,找一个自己最能理解的方式去实现。
写在最后
到这里,本文就结束了。最后,给出两个问题让大家来思考一下:
1.浏览器为什么要对接口的请求量做限制?
2.为什么要使用reduce,reduce处理异步的问题有什么优势吗?
如果有想法,可以直接在评论区回复哦。