问题存在背景
假设我们项目中,存在一个搜索页面,我们在页面的搜索框中,分别输入a,b,c查询了三次,我们期待的结果,页面肯定是显示c的查询接果,正常情况是这种,但时候有时候受网络波动的影响,接口响应应可能不会按照我们预期的那样返回。这种时候如果a的查询最先响应,但是b延迟了,c的接口响应回来,b才响应,a>c>b这种就是非我们的期待的返回结果。
说下解决方案
- 取消之前的请求
- 只取最新的请求结果
取消请求
实现思路(采用axios举例)
- 基于
axios.CancelToken来实现 - 使用一个
requestList储存请求信息的队列<url,cancenFn> - 在
axios.interceptors.request的请求拦截器中,对config添加cancenToken并赋值new CancelToken((c) => { cancel = c }) - 然后判断当前
url是否在requestList中存在,存在则取值,调用,不存在,则存储this.requestList.set(url, cancel) - 最后在
axios.interceptors.response中,在执行删除操作this.requestList.delete(url)
具体实现代码如下
GetTheLatestRequest.js
const CancelToken = axios.CancelToken
class GetTheLatestRequest {
// 储存请求信息的队列<url,cancenFn>
requestList = new Map()
constructor() {
this.addCancelFn = this.addCancelFn.bind(this)
this.delCancenFnByUrl = this.delCancenFnByUrl.bind(this)
}
addCancelFn (config) {
const { url } = config
let cancel
config.cancelToken = new CancelToken((c) => { cancel = c })
if (this.requestList.has(url)) {
const cancelFn = this.requestList.get(url)
cancelFn && cancelFn()
}
this.requestList.set(url, cancel)
return config
}
delCancenFnByUrl (url) {
this.requestList.delete(url)
}
}
export default GetTheLatestRequest
index.js
import GetTheLatestRequest from './GetTheLatestRequest.js'
const { addCancelFn, delCancenFnByUrl } = new GetTheLatestRequest()
// 添加请求拦截器
axios.interceptors.request.use((config) => {
return addCancelFn(config)
}, (error) => {
return Promise.reject(error)
})
// 添加响应拦截器
axios.interceptors.response.use((response) => {
delCancenFnByUrl(response.config.url)
return response
}, (error) => {
return Promise.reject(error)
})
export default axios
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>获取最新的接口请求</title>
</head>
<body>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">
import http from './index.js'
// 测试代码
for (let i = 1; i <= 10; i++) {
http.get(`./data.json`).then(res => {
console.log('then的回调')
console.log(`获取第${i}次请求`, res)
}).catch(err => {
console.log('catch的回调')
console.log(`取消第${i}请求`, err)
})
}
</script>
</html>
data.json
{
"code": 0,
"data": {
"name": "测试拦截重复请求"
}
}
执行结果 期待只在then回调中打印10,其他的则在catch回调中
通过运行发现,这个方案是符合我们的需求的,ok这个方案说完 来说另一种
只取最新的请求结果
实现思路(采用axios举例)
- 使用一个队列
requestList和自增id实现 - 使用一个
requestList储存请求信息的队列<url,[id]> - 在
axios.interceptors.request的请求拦截器中,对config添加$id赋值为++this.id - 然后判断当前
url是否在requestList中存在,存在则继续往值里面追加this.requestList.get(url).push(config.$id),不存在,则存储this.requestList.set(url, [id]) - 最后在
axios.interceptors.response中,通过url取值进行判断,没有则说明该接口的最新响应结果,已经被处理,直接结束,有值,进行取值判断,用数组的最后一个value.at(-1)和当前config.$id进行比较,相等,则说明是最新的请求接口,进行处理,否则还是不处理。
具体实现代码如下
GetTheLatestRequest.js
class GetTheLatestRequest {
// 储存请求信息的队列<url,Id>
requestList = new Map()
id = 0
constructor() {
this.addId = this.addId.bind(this)
this.isUseResponseData = this.isUseResponseData.bind(this)
}
addId (config) {
const { url } = config
config.$id = ++this.id
if (this.requestList.has(url)) {
const ids = this.requestList.get(url)
ids.push(config.$id)
} else {
this.requestList.set(url, [config.$id])
}
return config
}
isUseResponseData (response) {
const { url, $id } = response.config
if (this.requestList.has(url)) {
const value = this.requestList.get(url)
// 因为id是累加的,所以数组的最后的末尾一定是最新的请求ID,所以只需要比较config.$id来比较即可
if (value.at(-1) === $id) {
this.requestList.delete(url)
return true
} else {
return false
}
}
return false
}
}
export default GetTheLatestRequest
index.js
import GetTheLatestRequest from './GetTheLatestRequest.js'
const { addId, isUseResponseData } = new GetTheLatestRequest()
// 添加请求拦截器
axios.interceptors.request.use((config) => {
return addId(config)
}, (error) => {
return Promise.reject(error)
});
// 添加响应拦截器
axios.interceptors.response.use((response) => {
const value = isUseResponseData(response)
return value ? response : Promise.reject('不是最新请求')
}, (error) => {
return Promise.reject(error)
})
export default axios
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>获取最新的接口请求</title>
</head>
<body>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">
import http from './index.js'
// 测试代码
for (let i = 1; i <= 10; i++) {
http.get(`./data.json`).then(res => {
console.log('then的回调')
console.log(`获取第${i}次请求`, res)
}).catch(err => {
console.log('catch的回调')
console.log(`取消第${i}请求`, err)
})
}
</script>
</html>
data.json
{
"code": 0,
"data": {
"name": "测试拦截重复请求"
}
}
执行结果 期待只在then回调中打印10,其他的则在catch回调中
基于axios的解决方案就说完了,再说了另外一个
大家都知道vue的watchEffect函数吧
function watchEffect( effect: (onCleanup: OnCleanup) => void, options?: WatchEffectOptions ): StopHandle
我们来看下官网的对它的描述
第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求。
用通俗的话说,假如你的watchEffect,首行打印变量,然后在onclearup函数中,再次打印了变量,后面使用延时器(1s)修改这个变量,然后页面查看,你会发现首行的代码打印了两次,onclearup第二次才打印
import { watchEffect, ref } from 'vue'
const count = ref(10)
setTimeout(() => {
count.value--
}, 1000)
watchEffect((onCleanup) => {
console.log(count.value)
onCleanup(() => {
console.log('我是上次注册的回调函数')
})
})
其实就是我们() => { console.log('我是上次注册的回调函数') }我们传入的这个回调函数,没有被立即执行,而是保存了起来,下次才会触发,我们用这个机制就可以实现,我们的请求只拿最新的请求结果。 大概实现思路就是:就是在watchEffect声明一个变量标识,默认是true,在onclearup的回调里面修改为false,然后在接口响应判断,当前标识为true,我们就接收当前的响应结果,否则不处理
验证方案
我们声明一个变量requestCount设置为5,模拟5次请求,加上watcheffect是一个立即运行函数,所以预期是发送6次请求,我们只拿到第6次请求,其他过滤 ,具体代码如下
index.vue
<template>
<div class="block">
看控制台
</div>
</template>
<script lang="ts" setup>
import useRequest from '../../hooks/useRequest'
import type { ResponseData } from '../../hooks/useRequest'
let requestCount = 5
let index = 1
let timer: any
const request = (): Promise<ResponseData> => {
return new Promise((resolve) => {
let currentIndex = index
index++
console.log(`第${currentIndex}个发送的请求`, `响应的时长${1000 * requestCount}`)
setTimeout(() => {
resolve({
msg: `我是第${currentIndex}个的响应的请求`,
code: 0,
data: {
list: [1],
total: 20
}
})
}, 1000 * requestCount)
})
}
const { pagination } = useRequest(request)
timer = setInterval(() => {
pagination.pageNum = requestCount
requestCount--
if (requestCount === 0) {
clearInterval(timer)
}
}, 1000)
</script>
useRequest.ts
import { shallowRef, ref, reactive, watchEffect } from 'vue'
export interface ResponseData {
msg: string
code: number,
data: {
list: Array<unknown>
total: number
}
}
interface Pagination {
pageSize: number,
pageNum: number
}
type Key = keyof Pagination
interface Api {
(val: Pagination): Promise<ResponseData>
}
const useRequest = (api: Api) => {
const dataList = shallowRef<Array<unknown>>([])
const sumTotal = ref(0)
const count = ref(10)
const pagination = reactive({
pageSize: 1,
pageNum: 10
})
const handleRequest = (flag: { value: boolean }) => {
api(pagination).then(res => {
if (flag.value) {
console.log(res)
const { total, list } = res.data
dataList.value = list
sumTotal.value = total
}
})
}
watchEffect((onCleanup) => {
// 告诉监听器,我使用了pagination的所有属性,一旦属性发生变化,就可以触发
const keys = Object.keys(pagination) as Array<Key>
keys.forEach((key) => pagination[key])
let isUseCurrentRequestDataFlag = true
onCleanup(() => {
isUseCurrentRequestDataFlag = false
})
handleRequest({
get value() {
return isUseCurrentRequestDataFlag
}
})
})
return {
count,
dataList,
sumTotal,
pagination
}
}
export default useRequest
预期接口应该是handleRequest这个函数的console.log(res)只打印一次
我们来看执行代码
符合预期,至此完成了