网络数据竞态问题:由于网络不稳定性,后建议的ajax恳求有或许会比先建议的ajax拿到呼应成果,带来的问题便是数据处理的次序反常。

场景:一般出现在频频建议恳求的展现性视图中,如远程查找输入框、分页快速切换等,有或许会出现展现成果与查找不符

封装撤销上一次 axios 恳求 或 promise 的 ts 高阶函数,处理网络数据竞态问题

处理方案: 只对最终一次恳求数据进行处理

  1. 撤销上一次的 axios 恳求
  2. 撤销上一次的 promise
  3. 自增 id 只履行最新 id 的处理

至于 rxjsswitchMap 这儿就不过多介绍了,感兴趣的能够自行检查

目前提供这些处理办法的文章现已许多,可是少有用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
}
  • 简略运用
  1. 在 api 文件直接包一层 cancelAxios 装备一下 signal 就行
  2. 事务组件 调用 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需求做非空校验

参考及特别感谢:字节跳动技能团队