封装取消上一次 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需要做非空校验

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

相关推荐
江城开朗的豌豆5 分钟前
Vue路由动态生成秘籍:让你的链接'活'起来!
前端·javascript·vue.js
晓得迷路了6 分钟前
栗子前端技术周刊第 88 期 - Apache ECharts 6.0 beta、Deno 2.4、Astro 5.11...
前端·javascript·echarts
江城开朗的豌豆11 分钟前
在写vue公用组件的时候,怎么提高可配置性
前端·javascript·vue.js
江城开朗的豌豆12 分钟前
Vue路由跳转的N种姿势,总有一种适合你!
前端·javascript·vue.js
江城开朗的豌豆12 分钟前
Vue路由玩法大揭秘:三种路由模式你Pick谁?
前端·javascript·vue.js
江城开朗的豌豆13 分钟前
Vue路由守卫全攻略:给页面访问装上'安检门'
前端·javascript·vue.js
前端 贾公子21 分钟前
monorepo + Turborepo --- 开发应用程序
java·前端·javascript
江城开朗的豌豆25 分钟前
Vue路由传参避坑指南:params和query的那些猫腻
前端·javascript·vue.js
十里青山33 分钟前
超好用的vue图片预览插件更新啦,hevue-img-preview 7.0.0版本正式发布,支持vue2/vue3/移动/pc,增加缩略图、下载、自定义样式等
前端·javascript·vue.js
lichenyang45342 分钟前
css模块化以及rem布局
前端·javascript·css