一文学会请求并发限制、请求结果缓存、定时释放缓存

前言

前段时间也写过一篇关于前端控制网络请求的文章"一文学会请求中断、请求重发、请求排队、请求并发",由于时间原因没有把请求并发做详细介绍,今天利用空余时间再单独写一篇文章进行展开详细说明。

封装请求方法类

js 复制代码
import axios from './index'
import UseQueue from "./hooks/UseQueue"

// 使用请求并发队列
const useQueue = new UseQueue()

class Request {
  get(url, params) {
    return new Promise((resolve, reject) => {
      useQueue.request({
        requestFn: () => {
          return axios.get(url, {
            params
          })
        },
        resolve,
        reject,
        // 做请求缓存时使用(相同url使用同一请求缓存结果)
        key: url
      })
    })
  }
  delete(url, params) {
    return axios.get(url, {
      params
    })
  } 
  post(url, data) {
    return axios.post(url, data)
  }
  put(url, data) {
    return axios.put(url, data)
  }
}

export default new Request();

封装请求并发维护类

count: 设置请求并发数

current: 标记当前正在请求数量

waitQueue: 维护等待请求的任务队列

js 复制代码
export default class UseQueue {
  constructor() {
    // 请求等待队列
    this.waitQueue = []
    // 限制请求并发数
    this.count = 2
    // 记录当前正在请求的数量
    this.current = 0
  }
  request({
    requestFn,
    resolve,
    reject
  }) {
    if(this.current < this.count) {
      // 当前正在请求数量小于限制并发数,直接发送请求 
      requestFn().then(res => {
        // 接口请求成功
        console.log('请求成功', res)
        resolve(res)  // 请求成功结果Promise返回给页面
      }).catch(err => {
        // 接口请求失败
        console.log('请求失败', err)
        reject(err)   // 请求失败结果Promise返回给页面
      }).finally(() => {
        console.log('完成')
        // 请求完成接着下一个请求
        this.complete()
      })
      // 发送请求后当前请求数+1
      this.current++
    } else {
      // 超出并发数量,加入请求等待队列
      this.waitQueue.push({
        requestFn,
        resolve,
        reject
      })
    }
  }
  complete() {
    // 请求完成正在请求数量减一
    this.current--
    // 等待队列还有未发送请求,继续下一个请求
    if(this.waitQueue.length > 0) {
      // 从等待队列开头发送请求(先进先出)
      const config = this.waitQueue.shift()
      // 发送请求
      this.request(config)
    } else {
      console.log('等待请求队列为空了!')
    }
  }
}

这样就可以实现一个简单的限制请求并发类了! 我们可以写一段代码测试一下

js 复制代码
for(let i = 1; i <= 60; i++) {
    test(Math.ceil(i / 10)).then((res) => {
      console.log(res)
      this.list.push(res)
    })
}

下图 可看到,每次最多只能发送两个接口,这是因为我们设置了最大并发数为2

请求结果缓存

假设 我们封装了一个业务组件,组件使用时需要依赖后台接口返回数据才能正常使用,而产品页面设计图又需要这个组件在页面上同时多次渲染 ,则会多次请求相同接口,这时我们可以对请求结果进行缓存,当第一次接口请求成功后,后续相同请求直接通过Promise返回第一次请求成功的结果数据。

UseQueue类中添加

cache:请求结果缓存Map

getCache: 获取请求缓存结果方法

js 复制代码
export default class UseQueue {
  constructor() {
    ...省略以上代码
    
    // 相同请求缓存(假定请求url一样为相同请求)
    this.cache = new Map()
  }
  // 获取缓存
  getCache(key, fn) {
    // 存在缓存
    if(this.cache.has(key)) {
      return {
        request: this.cache.get(key).request,
        // 命中缓存
        isCache: true
      }
    }
    const request = fn()
    // 命中缓存请求数不变,发送新请求后请求数+1
    this.current ++
    this.cache.set(key, {
      request
    })
    return {
      request,
      // 没有命中缓存
      isCache: false
    }
  }
  request({
    requestFn,
    resolve,
    reject,
    key
  }) {
    // 考虑正在请求和等待队列是否存在相同请求,如果存在则使用相同请求的返回结果
    if(this.current < this.count) {
      // 当前正在请求数量小于限制并发数,直接发送请求 
      const { request, isCache } = this.getCache(key, requestFn)
      request.then(res => {
        // 接口请求成功
        console.log('请求成功', res)
        resolve(res)  // 请求成功结果Promise返回给页面
      }).catch(err => {
        // 接口请求失败
        console.log('请求失败', err)
        reject(err)   // 请求失败结果Promise返回给页面
      }).finally(() => {
        console.log('完成')
        // 请求完成接着下一个请求
        this.complete(isCache)
      })
    } else {
      // 加入请求等待队列
      this.waitQueue.push({
        requestFn,
        resolve,
        reject,
        key
      })
    }
  }
  complete(isCache) {
    // 请求完成正在请求数量减一(命中缓存则不用减)
    if(!isCache) this.current--
    // 等待队列还有未发送请求,继续下一个请求
    if(this.waitQueue.length > 0) {
      // 从等待队列开头发送请求(先进先出)
      const config = this.waitQueue.shift()
      // 发送请求
      this.request(config)
    } else {
      console.log('等待请求队列为空了!')
    }
  }
}

到这里,请求缓存也基本完成了!我们还是拿上次的for循环调用接口代码测试一下。

js 复制代码
for(let i = 1; i <= 60; i++) {
    test(Math.ceil(i / 10)).then((res) => {
      console.log(res)
      this.list.push(res)
    })
}

我们这里请求了60次接口,a1/a2/a3/a4/a5/a6接口各请求了10次。但是下图可发现只请求了6次接口,每个接口只请求了一次,这就是做了请求结果缓存。

But,这还存在弊端,后续调用接口会一直拿缓存数据,这时需要设置一个缓存过期时间,过期后重新请求后台接口。

设置缓存过期时间

UseQueue类中添加

cacheTime:缓存时间(毫秒)

isExpired: 判断缓存是否过期方法

发送请求时记录当前时间和当前请求一起保存到Map缓存中

getCache方法中取缓存时先判断是否过期,过期则调用接口。

js 复制代码
export default class UseQueue {
  constructor() {
    ...
    // 缓存时间(毫秒)
    this.cacheTime = 1000 * 10
  }
  request({
    requestFn,
    resolve,
    reject,
    key
  }) {
  // 判断是否过期
  isExpired(key) {
    const currentTime = Date.now()
    return currentTime - this.cache.get(key).createTime > this.cacheTime
  }
  // 获取缓存
  getCache(key, fn) {
    // 存在缓存,并且没有过期
    if(this.cache.has(key) && !this.isExpired(key)) {
      return {
        request: this.cache.get(key).request,
        // 命中缓存
        isCache: true
      }
    }
    // 获取当前缓存时间
    const currentTime = Date.now()
    const request = fn()
    // 命中缓存请求数不变,发送新请求后请求数+1
    this.current ++
    this.cache.set(key, {
      request,
      createTime: currentTime
    })
    return {
      request,
      // 没有命中缓存
      isCache: false
    }
  }
  ...
}

这时,当超过设定的缓存时间时,就会重新调用接口获取数据,但当请求接口多了,数据会一直缓存,可能导致内存溢出,所以还得定时清除过期的缓存数据。

定时清除缓存

UseQueue类中添加

timer: 清理过期缓存定时器

startTimer: 开启定时器方法

clearExpiredCache: 清除过期缓存方法

getCache方法中,记录发送请求时间时,开启清除缓存定时器。

js 复制代码
export default class UseQueue {
  constructor() {
   ...
    // 清理过期缓存定时器
    this.timer = null
  }
  ...
  // 获取缓存
  getCache(key, fn) {
    ...
    // 缓存后,开启定时器,时间到自动清理
    this.startTimer()
    return {
      request,
      // 没有命中缓存
      isCache: false
    }
  }
  // 释放过期缓存内存
  clearExpiredCache() {
    if(this.cache.size !== 0) {
      for (const [key] of this.cache.entries()) {
        if(this.isExpired(key)) {
          this.cache.delete(key)
        }
      }
    }
  }
  // 开启定时器
  startTimer() {
    clearTimeout(this.timer)
    this.timer = setTimeout(() => {
      this.clearExpiredCache()
      this.timer = null
      console.log('清理完成', this.cache)
    }, this.cacheTime);
  }
}

结语

实现以上需求关键在于掌握Promise,通过Promise的等待机制,可以较好控制请求发送时机;当一次请求成功后,Promise状态变为已完成(Fulfilled),后续相同请求可以共用这个Promise,直接通过.then从中获取数据,从而实现缓存请求结果的效果。

完整源码

UseQueue.js

js 复制代码
/**
 * 1.限制请求并发数
 * 2.请求结果缓存(相同url接口使用同一返回结果)
 * 3.定时释放缓存结果内存
 */

export default class UseQueue {
  constructor() {
    // 请求等待队列
    this.waitQueue = []
    // 限制请求并发数
    this.count = 2
    // 记录当前正在请求的数量
    this.current = 0
    // 相同请求缓存(假定请求url一样为相同请求)
    this.cache = new Map()
    // 缓存时间(毫秒)
    this.cacheTime = 1000 * 10
    // 清理过期缓存定时器
    this.timer = null
  }
  request({
    requestFn,
    resolve,
    reject,
    key
  }) {
    // 考虑正在请求和等待队列是否存在相同请求,如果存在则使用相同请求的返回结果
    if(this.current < this.count) {
      // 当前正在请求数量小于限制并发数,直接发送请求 
      const { request, isCache } = this.getCache(key, requestFn)
      request.then(res => {
        // 接口请求成功
        console.log('请求成功', res)
        resolve(res)  // 请求成功结果Promise返回给页面
      }).catch(err => {
        // 接口请求失败
        console.log('请求失败', err)
        reject(err)   // 请求失败结果Promise返回给页面
      }).finally(() => {
        console.log('完成')
        // 请求完成接着下一个请求
        this.complete(isCache)
      })
    } else {
      // 加入请求等待队列
      this.waitQueue.push({
        requestFn,
        resolve,
        reject,
        key
      })
    }
  }
  complete(isCache) {
    // 请求完成正在请求数量减一(命中缓存则不用减)
    if(!isCache) this.current--
    // 等待队列还有未发送请求,继续下一个请求
    if(this.waitQueue.length > 0) {
      // 从等待队列开头发送请求(先进先出)
      const config = this.waitQueue.shift()
      // 发送请求
      this.request(config)
    } else {
      console.log('等待请求队列为空了!')
    }
  }
  // 获取缓存
  getCache(key, fn) {
    // 存在缓存,并且没有过期
    if(this.cache.has(key) && !this.isExpired(key)) {
      return {
        request: this.cache.get(key).request,
        // 命中缓存
        isCache: true
      }
    }
    // 获取当前缓存时间
    const currentTime = Date.now()
    const request = fn()
    // 命中缓存请求数不变,发送新请求后请求数+1
    this.current ++
    this.cache.set(key, {
      request,
      createTime: currentTime
    })
    // 缓存后,开启定时器,时间到自动清理
    this.startTimer()
    return {
      request,
      // 没有命中缓存
      isCache: false
    }
  }
  // 释放过期缓存内存
  clearExpiredCache() {
    if(this.cache.size !== 0) {
      for (const [key] of this.cache.entries()) {
        if(this.isExpired(key)) {
          this.cache.delete(key)
        }
      }
    }
  }
  // 判断是否过期
  isExpired(key) {
    const currentTime = Date.now()
    return currentTime - this.cache.get(key).createTime > this.cacheTime
  }
  // 开启定时器
  startTimer() {
    clearTimeout(this.timer)
    this.timer = setTimeout(() => {
      this.clearExpiredCache()
      this.timer = null
      console.log('清理完成', this.cache)
    }, this.cacheTime);
  }
}

request.js

js 复制代码
import axios from './index'
import UseQueue from "./hooks/UseQueue"

const useQueue = new UseQueue()

class Request {
  get(url, params, args) {
    console.log('args', args)
    return new Promise((resolve, reject) => {
      useQueue.request({
        requestFn: () => {
          return axios.get(url, {
            params
          })
        },
        resolve,
        reject,
        key: url
      })
    })
  }
  delete(url, params) {
    return axios.get(url, {
      params
    })
  } 
  post(url, data) {
    return axios.post(url, data)
  }
  put(url, data) {
    return axios.put(url, data)
  }
}

export default new Request();

往期文章

一文学会请求中断、请求重发、请求排队、请求并发

uniapp实现背景颜色跟随图片主题色变化(多端兼容)

table表格自适应浏览器窗口变化解决方案

一文学会vue3如何自定义hook钩子函数和封装组件

相关推荐
一丝晨光21 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
Front思22 分钟前
vue使用高德地图
javascript·vue.js·ecmascript
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http