网络数据竞态问题:由于网络不稳定性,后建议的ajax恳求有或许会比先建议的ajax拿到呼应成果,带来的问题便是数据处理的次序反常。
场景:一般出现在频频建议恳求的展现性视图中,如远程查找输入框、分页快速切换等,有或许会出现展现成果与查找不符
处理方案: 只对最终一次恳求数据进行处理
- 撤销上一次的 axios 恳求
- 撤销上一次的 promise
- 自增 id 只履行最新 id 的处理
至于 rxjs
的 switchMap
这儿就不过多介绍了,感兴趣的能够自行检查
目前提供这些处理办法的文章现已许多,可是少有用ts写的,也少有封装撤销上一次axios恳求的,本文用于个人学习记载,需求的时候能够直接cv用
1. 撤销 axios 恳求
代码比较简略,相信大家都知道能够通过 AbortController
来 撤销恳求,关键在于把 signal 暴露并接纳其余传参给回调以及自动撤销上一次的恳求,被撤销的恳求都会处于rejected
状况
type CancelPrevious = AbortController['abort'] | null
export default function cancelAxios<T extends any[], R>(
fn: (signal: AbortSignal, ...args: T) => R
): (...args: T) => R {
let controller: AbortController | null = null
let cancelPrevious: CancelPrevious = null
const wrappedFn: (...args: T) => R = (...args) => {
// 第一次调用 cancelPrevious 是 null,后面都会调用上一次记载的 controller.abort() 办法
cancelPrevious && cancelPrevious()
controller = new AbortController()
const signal = controller.signal
const result = fn(signal, ...args)
// 记载当时cancel的办法用于下一次调用履行
cancelPrevious = () => controller && controller.abort()
return result
}
return wrappedFn
}
- 简略运用
- 在 api 文件直接包一层 cancelAxios 装备一下 signal 就行
- 事务组件 调用 api,需求留意的是要区分Cancel反常或许其他反常,能够运用
axios.isCancel
或许error.code === 'ERR_CANCELED'
进行判别
// 1.api文件
export const findAll = cancelAxios((signal: AbortSignal, data) =>
axios.post('api/xxx', data, { signal })
)
// 2.事务组件 调用 api
import axios from 'axios'
const querySearch = async (queryString: string) => {
try {
const { data } = await findAll({ query: queryString })
} catch (error) {
if (axios.isCancel(error)) {
// ...
} else {
// ...
}
}
}
2. 撤销 Promise
和撤销axios恳求的处理类似,这儿的关键点是让 promise 一向处于 pending
状况来撤销链式调用。引证一个 npm 库 awesome-imperative-promise
来实现该功用,该库原理也很简略感兴趣的能够看一下 源码。至于大佬们评论的内存走漏问题,个人在最新版 chrome 123 中测验未发现,介怀慎用。
import { createImperativePromise } from 'awesome-imperative-promise'
import type { CancelCallback } from 'awesome-imperative-promise'
type CancelPrevious = CancelCallback | null
type Fn<T extends any[], R> = (...args: T) => Promise<R>
export default function cancelPromise<T extends any[], R>(fn: Fn<T, R>): Fn<T, R> {
let cancelPrevious: CancelPrevious = null
const wrappedFn: Fn<T, R> = (...args: T) => {
// 调用cancelPrevious()来撤销上一次的promise
cancelPrevious && cancelPrevious()
const result = fn(...args) as Promise<R>
const { promise, cancel } = createImperativePromise(result)
// 记载当时cancel的办法用于下一次调用履行
cancelPrevious = cancel
return promise
}
return wrappedFn
}
3. 只处理最新 id
封装思路都相同,这儿用 自增 id 作为标识,只处理最新的 id 呼应,这儿和办法2不同的是会等候恳求完成再根据id判别回来data或许undefined,所以promise会处于 fulfilled
状况。需求留意的是处理接口数据时需求判别一下是否为undefined,否则有或许会报错。
export default function useResolveLastReq<T extends any[], R>(
fn: (...args: T) => Promise<R>
): (...args: T) => Promise<R | undefined> {
let id = 0 // 标识每次恳求
const wrappedFn = async (...args: T): Promise<R | undefined> => {
const curId = id + 1 // 每次恳求的ID
id = curId // 最新的恳求ID
// 履行恳求
const data = await fn(...args)
try {
if (curId === id) {
return data
}
} catch (e) {
if (curId === id) {
throw e
}
}
}
return wrappedFn
}
总结
以上三种办法都能够实现只处理最终一次的恳求成果,处理网络数据竞态问题,可是一切办法都会将恳求会发到后端(包含abort网络恳求),所以只合适用于查询类的恳求,不合适用于创建或许更新类的恳求,各有优劣,可自行挑选
1.撤销 axios 恳求:
- 长处:被撤销的promise会处于
rejected
状况,不会有内存走漏的顾虑,且能够削减网络资源的浪费。 - 缺点:需求特别处理 Cancel 反常不影响用户体验。
2.撤销 promise:
- 长处:不用等候接口呼应能够直接撤销promise,只会履行最终一次的promise数据处理,不需求做undefined或Cancel rejected的特别处理
- 缺点:依靠第三方库(其实很小,就43行代码能够直接cv到本地运用),被撤销的promise都会处于
pending
状况,有或许会内存走漏,经测验个人觉得没有一向处于引证状况就能够定心用
3.只处理最新id
- 长处:简略通用
- 缺点:速度或许是最慢的,需求等候每一个promise处于
fulfilled
状况才进行有用数据处理,对回来成果undefined需求做非空校验
参考及特别感谢:字节跳动技能团队