前言
前段时间也写过一篇关于前端控制网络请求的文章"一文学会请求中断、请求重发、请求排队、请求并发",由于时间原因没有把请求并发做详细介绍,今天利用空余时间再单独写一篇文章进行展开详细说明。
封装请求方法类
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();