封装取消上一次 axios 请求 或 promise 的 ts 高阶函数,解决网络数据竞态问题

网络数据竞态问题:由于网络不稳定性,后发起的ajax请求有可能会比先发起的ajax拿到响应结果,带来的问题就是数据处理的顺序异常。

场景:一般出现在频繁发起请求的展示性视图中,如远程搜索输入框、分页快速切换等,有可能会出现展示结果与搜索不符

解决方案: 只对最后一次请求数据进行处理

  1. 取消上一次的 axios 请求
  2. 取消上一次的 promise
  3. 自增 id 只执行最新 id 的处理

至于 rxjsswitchMap 这里就不过多介绍了,感兴趣的可以自行查看

目前提供这些解决方法的文章已经很多,但是少有用ts写的,也少有封装取消上一次axios请求的,本文用于个人学习记录,需要的时候可以直接cv用

1. 取消 axios 请求

代码比较简单,相信大家都知道可以通过 AbortController取消请求,关键在于把 signal 暴露并接收其余传参给回调以及自动取消上一次的请求,被取消的请求都会处于rejected状态

ts 复制代码
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'进行判断
ts 复制代码
// 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 中测试未发现,介意慎用。

ts 复制代码
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,否则有可能会报错。

ts 复制代码
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需要做非空校验

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

相关推荐
十步杀一人_千里不留行2 小时前
React Native 下拉选择组件首次点击失效问题的深入分析与解决
javascript·react native·react.js
道不尽世间的沧桑3 小时前
第9篇:插槽(Slots)的使用
前端·javascript·vue.js
bin91533 小时前
DeepSeek 助力 Vue 开发:打造丝滑的滑块(Slider)
前端·javascript·vue.js·前端框架·ecmascript·deepseek
GISer_Jing4 小时前
Node.js中如何修改全局变量的几种方式
前端·javascript·node.js
秋意钟4 小时前
Element UI日期选择器默认显示1970年解决方案
前端·javascript·vue.js·elementui
程序员黄同学5 小时前
请谈谈 Vue 中的 key 属性的重要性,如何确保列表项的唯一标识?
前端·javascript·vue.js
繁依Fanyi5 小时前
巧妙实现右键菜单功能,提升用户操作体验
开发语言·前端·javascript·vue.js·uni-app·harmonyos
前端御书房5 小时前
前端防重复请求终极方案:从Loading地狱到精准拦截的架构升级
前端·javascript
程序员黄同学5 小时前
解释 Vue 中的虚拟 DOM,如何通过 Diff 算法最小化真实 DOM 更新次数?
开发语言·前端·javascript
果粒chenl6 小时前
css+js提问
前端·javascript·css