控制并发请求及结果返回
我们都知道,浏览器在发送请求的时候,有最大并发数量的限制,如果在我们的网页中同时有大量的资源需要加载,那么浏览器不会同时加载这些数据,而是先发出一部分请求,等到有一个请求数据响应完毕的时候,才会发出另一个请求。
这里的最大并发请求数量不是越大或者越小就好,需要找到一个相对的平衡点,因为并发数越少,那么各个请求之间发生阻塞的次数就会增多,导致页面加载过慢,影响用户体验,这就好比我们从北京到上海,如果可乘坐的车次很少,那么大量的人员就有可能抢不到票,导致我们只能延期出行,只能等待上次列车返回之后才能乘坐下一次列车。
火车的数量与单列的承载量
那么是不是可乘坐的列车次数(放到浏览器中就是请求并发数)越大就越好呢?显然不是,列车需要铁轨与工作人员,并且要处理好调度事宜,同样并发越大服务器承受的压力越大,解析请求的时间越耗时。因此适当的增加每趟列车的车厢数,就可以在列车数与运载量之间找到平衡,放到实际的项目中就是合并我们的js、css等文件,将多个请求合并为一个请求,达到优化网站的目的。
(Tips:该限制是针对同一个域名作用的,可以使用不同子域名的方式来突破这个限制。)
目前浏览器中对这个的限制数量一般为6个。
那么我们能不能通过代码来实现一个类似的功能,自己定义一个方法来控制最大的并发请求数量呢?接下来我们就尝试做到这一点。
当然可以的啦
建立一个待请求队列queue,根据我们设定的最大并发请求数量max,来同时发起max个请求,当其中有任意一个请求响应并返回值的时候,我们将队列中的下一个请求立即发送,以此类推,直到所有请求全部发送完毕。可以将所有请求的返回结果缓存起来,然后当所有请求全部响应完成之后,返回一个结果数组,按照相应的队列顺序把结果抛给调用者,并且执行预先设定好的回调函数,如果存在的话。
首先,我们需要模拟出来一大堆异步请求,当然是选择promise来处理这项工作啦。为了简单起见,可以通过一个函数创建待执行的promise,并在promise里面执行相应异步操作。
function createPromise (num) {
console.log(`生成第${num}个promise`)
return function () {
return new Promise((resolve, reject) => {
let time = Math.floor(Math.random() * 90) + 10
console.log(`执行第${num}个promise,需耗时${time}毫秒`)
setTimeout(() => {
console.log(`已完成请求${num}`)
resolve({
order: num,
data: `结果${num}`
})
}, time)
})
}
}
为了方便演示,我们定义一个createPromise函数,它返回一个待执行函数,这个函数会返回一个promise,通过传入一个num参数来作为区分不同异步操作的标识符,实际项目中可以直接对多个异步操作进行控制,不需要多包裹的一层function。
其中promise里面,我们定义了一个随机的接口请求时间time,设定为10ms~100ms之间,来模拟我们的真实请求环境,因为各个接口的实际访问时间都是不尽相同。然后利用setTimeout来模拟异步操作,当在对应time时间内接口相应完成之后,将会把得到的返回值resolve出去,以便做进一步的处理。
这里resolve的结果是一个对象,order属性用来标识该异步操作的身份,data属性用来存放异步操作的结果。
[ok]这里没什么技术含量,现在我们通过这个函数来创建我们需要的东西。
接下来,我们通过一个循环将多次生成的函数返回值存放到一个队列里面,以供我们将来使用。
var queue = []
for (let i = 1; i < 11; i++) {
queue.push(createPromise(i))
}
好了,现在queue数组里面存放了10个待执行函数,到此为止我们的准备工作已经做好了。
嘻嘻~
异步请求队列准备完毕,那么我们需要创建一个limitRequest函数,来处理我们最开始的诉求,我们希望,这个函数应该是这样调用的。
limitRequest(queue, 4, function (data) {
console.log(data)
console.log("所有请求全部执行完毕")
})
limitRequest第一个参数为我们刚创建的请求队列,第二个参数为我们要限制的最大并发请求数量,第三个参数为全部请求执行完毕的回调函数,该函数传入的参数为所有请求返回值组成的一个数组,结果的位置与相应的请求在队列中的位置一一对应。
现在我们就来构造这个函数,该如何入手呢?咳咳~,忍不住想说句题外话。[泪奔]
任何的项目,代码层面都是最不重要的,到了写代码这一步的时候,劳动力都是最廉价的,从项目角度来说,业务!业务!业务!才是最重要的,只有理清了业务我们才能从更高的角度来设计出一个优秀的系统。从单个的问题或者需求来说,不断的分析或者推演之后得到的结论才能更好的指导我们进行开发。
举一个不太恰当的例子,就比如管中窥豹,如果我们只通过一根管子来看对面的物体,不移动管子,只看了一眼之后,我们就描绘它的形状,根据我们的经验,我们会觉得这是一头豹子,但是其实它可能是一只斑点狗或者一只大花猫,甚至可能是其他的什么动物或物体,当我们移动一下发现自己错了的时候,那么就需要修正自己的见解,如此反复下去就像我们写代码一样,bug反反复复的,这里看着对了,但是那里又错了。如果我们一开始不着急下结论,而是同样拿着这个管子围绕这个物体上下左右前后多个角度仔细的观察之后再下结论,那么正确的概率将极大的增加,并且后期只需要极小的修补就可以打到满意的效果。
(⊙o⊙)…
闭嘴吧,真能墨迹
言归正传,回忆一下刚才我们所做的事情,分析一下我们要做的处理,发起多个请求→等待一个返回→追加一个请求→继续等待返回。其实这是一个不断的控制权转让的问题。那么我们考虑使用generator来帮助我们完成这个事情,并且我们要做好计数,即当请求数达到最大的时候就停止继续发送请求,当有请求返回的时候,我们继续恢复发送请求,直到所有请求全部执行完毕,然后将获得到的结果全部放在数组里面返回出去,在这期间我们要把获得的结果缓存起来,放在数组里面,并在最后通知generator函数结束执行。根据这个思路,请看下面的代码,我们会发现非常的清晰。[灵光一闪]
function limitRequest (q, max, cb) {
console.log(`请求最大并发数控制为${max}`)
let request = generatorRequest(q)
let sent = 0
let received = 0
let result = []
let size = Math.min(q.length, max)
function runIt () {
request.next().value.then(value => {
result[value.order - 1] = value.data
received++
if (sent < q.length) {
sent++
runIt()
} else if (received == q.length) {
request.return()
cb && cb(result)
}
})
}
while (sent < size) {
sent++
runIt()
}
}
天才不是我,是你,给自己一个掌声。[小鼓掌]
愣住
其中generatorRequest函数就是我们刚才说的generator,我们把对它的控制权用request来表示。我们先不必关心它,下面会讲到。sent表示已经发送的请求数量,received表示已经响应的请求数量,result表示返回结果的数组,其中size用来取所有待请求的数量与所设定的最大数量中的最小值,接下来就是核心点runIt函数。
request.next()将会执行generator函数并在yield表达式处释放控制权,并将yield后面的结果存放在value里面,由于得到的结果是一个promise,因此我们可以通过then来继续处理接下来的工作,主要做了三件事情:
其中while用来处理第一发出的最大数量的所有请求,后续的调用都通过runIt回调就可以了。
最终我们就来揭开generator神秘的面纱,其实很简单。[吐舌]
function* generatorRequest (q) {
let t = 0
while (true) {
yield q[t++]()
}
}
喏~,就这么几行,哈哈哈哈哈哈,其实这里偷了一个懒,直接初始化t之后就让它单纯的自增了,根据generator函数的性质,还是可以做到很多更精细化和好玩的控制的。
现在我们执行一下就会得到上面最开始的那张截图的例子的效果了,也就达成了我们刚开始给自己设定的需求。
撒花
好啦~今天的分享到此为止。[比心]
本人能力有限,希望借此分享可以帮助大家,如果有误导的地方实属抱歉!
祝可爱的你越来越棒!!![做鬼脸]加油~~~
留言与评论(共有 0 条评论) “” |