并发请求控制

467 阅读1分钟

并发请求控制

我们先看一个需求,也就是本文要解决的问题,当某一段时间内,触发了很多次请求,此时,服务器的压力非常大,因为前端基本上都是并发请求,并没有像流式那样去请求

所谓的流式请求,就是当前一个请求兑现之后,再请求下一个,这里的一个也可以是多个,前仆后继,当然这里的流式请求是笔者取的名字。

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);
 })
}

此时我们看浏览器是怎么处理的

image.png 我们可以清晰的看出,浏览器会像你所写的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 })
}

测试结果如下

可以明显看出,一段时间内至多只有三个请求实体在工作,当完成其中一个,后续请求实体会补上,就完成了并发的流式请求

image.png

我们把视线拉入代码里面

思考🤔:实现的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);
  })
}

image.png 好的,这样实现的好处是,concurrencyRequest更加自由的抽象,run函数可以在任何地方使用,并能享受流式的并发控制

总结

求求了,我想找个班上!!!

git仓库:github.com/CCherry07/d…