并发请求控制
我们先看一个需求,也就是本文要解决的问题,当某一段时间内,触发了很多次请求,此时,服务器的压力非常大,因为前端基本上都是并发请求,并没有像流式那样去请求
所谓的流式请求,就是当前一个请求兑现之后,再请求下一个,这里的一个也可以是多个,前仆后继,当然这里的流式请求是笔者取的名字。
const request = (options: { url: string }) => {
return fetch(options.url).then(res => res.json())
}
for (let i = 1; i <= 12; i++) {
const url = `https://jsonplaceholder.typicode.com/todos/${i}`;
request({ url }).then(res => {
console.log('then', res);
})
}
此时我们看浏览器是怎么处理的
我们可以清晰的看出,浏览器会像你所写的javascript同步执行代码一样,在一小段时间内并发多次请求
前端该如何解决这个问题呢?
我想就是控制,网络请求函数的执行时机,先收集所有的请求实体,当前一个实体兑现后,主动执行下一个实体,我们如何知道前一个实体是否兑现呢?
在前端,大部分的网络请求Api都实现了Promise A+ 规范,所以我们可以通过判断请求实体的Promise的状态,就可知晓请求是否已兑现
我们可以实现一个高阶函数concurrencyRequest,参数为(请求工厂函数,options),options存在 类型为number的max属性,此属性控制并发的最大数, concurrencyRequest返回一个函数我们取名为run,run函数接受的参数也就是请求工厂函数的参数,创建tasks收集请求实体的参数集队列,max属性是tasks队列的出口大小(这里就控制了并发最大数),定义running判断目前正在工作的请求的个数,在函数体中定义next函数,
此函数通过闭包机制维护着 tasks running 属性,这个函数才是发送真正请求的函数,此函数可以在前一个请求实体兑现后,不管是兑现后的状态是fulfilledorrejected,都会再次被调用,也就流转到下一个实体,就实现了流式请求
interface Options {
max: number
... // 其他扩展特性
}
const concurrencyRequest = <P extends Object, R = any>(request: (args: P) => Promise<R>, options: Options) => {
const tasks: P[] = [] // task 队列
const res = []
let running = 0
const max = options.max
function next() {
if (running >= max) return
running++
const opt = tasks.shift()!
return request(opt)
.then(response=>res.push(response))
.finally(() => {
running-- //不管失败还是成功running 都要--
if (tasks.length) {
next()
}
})
}
return (opt: P) => {
tasks.push(opt)
next()
}
}
我们来测试一下concurrencyRequest
这里笔者,不太会单元测试,只能从代码以及浏览器表现来测试了
const run = concurrencyRequest(request, { max: 3 })
for (let i = 1; i <= 12; i++) {
const url = `https://jsonplaceholder.typicode.com/todos/${i}`;
run({ url })
}
测试结果如下
可以明显看出,一段时间内至多只有三个请求实体在工作,当完成其中一个,后续请求实体会补上,就完成了并发的流式请求
我们把视线拉入代码里面
思考🤔:实现的run函数存在着什么问题?
···此处略n分钟
我们是不是没有返回值?哈哈哈哈,哪用起得多麻烦!可能会想,在concurrencyRequest函数体内存储请求的结果,并统一返回?这样又会出现什么问题呢?这样处理的话,只能在当前要做并发控制的时候,才能享受。也就是说,我们平时所写的 Promise.all(["url1","url2",...].map(request)) 才能享受。所以我要解决的问题是,将每一个请求分离,每一个请求实体都有一个自己独立的Promise状态返回,说人话就是,run函数的执行,会返回对应的Promise.
我给出的解决方案是:在实现run函数时,返回独立的Promise,并将Promise的控制权交由next函数中请求工厂函数管理,将Promise状态管理后置,也就是控制反转
说人话就是,返回的Promise的 resolve reject 回调函数,作为next函数中请求工厂函数返回的Promise.then参数调用
简单demo:
const executorCallBack: ExecutorCallBack<any> = {
resolve: null,
reject: null
}
function bar() {
return request().then(executorCallBack.resolve, executorCallBack.reject)
}
function foo() {
return new Promise((resolve, reject) => {
executorCallBack.reject = reject
executorCallBack.resolve = resolve
})
}
foo().then(...)
bar()
创建映射关系 executorMap,将每一次run函数执行的参数对象,作为key,run函数返回的Promise的ExecutorCallBacks作为value,一一对应,在next函数流转时,取值,兑现,销毁.
concurrencyRequest
const concurrencyRequest = <P extends Object, R = any>(request: (args: P) => Promise<R>, options: Options) => {
const tasks: P[] = [] // task 队列
const executorMap: WeakMap<P, ExecutorCallBack<any>> = new WeakMap()
let running = 0
const max = options.max
function next() {
if (running >= max) return
running++
const opt = tasks.shift()!
const { resolve, reject } = executorMap.get(opt)!
return request(opt)
.then(resolve, reject)
.finally(() => {
executorMap.delete(opt)
running-- //不管失败还是成功running 都要--
if (tasks.length) {
next()
}
})
}
return <T extends R>(opt: P): Promise<T> => {
return new Promise((resolve, reject) => {
tasks.push(opt)
executorMap.set(opt, { resolve, reject })
next()
})
}
}
这里有一个技巧,我使用WeakMap数据结构保存了resolve, reject,key为run函数传入的opt,虽然是弱引用,但是我不知道,run函数执行完之后,opt是否被回收,所以后面我还是,手动释放了映射🐶
测试
const run = concurrencyRequest(request, { max: 3 })
for (let i = 1; i <= 12; i++) {
const url = `https://jsonplaceholder.typicode.com/todos/${i}`;
run({ url }).then(res => {
console.log(res);
})
}
好的,这样实现的好处是,
concurrencyRequest更加自由的抽象,run函数可以在任何地方使用,并能享受流式的并发控制
总结
求求了,我想找个班上!!!
git仓库:github.com/CCherry07/d…