如何处理前端开发中的竞态请求

前言

竞态条件(Race Conditions)在前端开发中是一种常见的问题,特别是在多个异步操作同时竞争资源或执行时。这可能导致意外的结果,如数据不一致、重复请求和UI错误。

举个🌰

一个最常见的场景就是选项卡切换,用户快速切换Tab,由于网络延迟等原因,很可能最后停留的Tab所展示的内容,并不是用户想要看到的内容

出现这种情况是因为,用户最先点击的Tab请求把最后点击的Tab请求覆盖了(由于网络原因,最先发出的请求最后到达):

解决方案

取消请求

通常我们可以在新的请求发起之前,将旧的、未到达的请求给取消掉,这样旧的请求就不会覆盖新的请求了

XMLHttpRequest 取消请求

  • XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。
  • 如果请求已被发出,可以使用 abort() 方法立刻中止请求。
javascript 复制代码
const xhr= new XMLHttpRequest();

xhr.open('GET', 'https://xxx');
xhr.send();
// 取消请求
xhr.abort();

Fetch API 取消请求

  • fetch 号称是 AJAX 的替代品,出现于 ES6,它也可以发出类似 XMLHttpRequest 的网络请求。
  • 主要的区别在于 fetch 使用了 Promise,要中止 fetch 发出的请求,需要使用 AbortController。
javascript 复制代码
const controller = new AbortController();
const signal = controller.signal;

fetch('/xxx', {
  signal,
}).then(function(response) {
  //...
});
// 取消请求
controller.abort();

Axios cancel Token 取消请求

  • 相比原生 API,大多项目都会选择 axios 进行请求。
javascript 复制代码
import Axios from 'axios'

const CancelToken = Axios.CancelToken
let cancel

export const getSomeResource = (params: GetSomeResourceReq) => {
  if (cancel) {
    cancel()
  }
  const res = Axios.post('/xxx', params)
  return res.catch((err) => { // 取消了axios请求会走到异常处理,我们需要对这种错误进行过滤
    if (Axios.isCancel(err)) {
      return {} as GetSomeResourceRsp
    }
    throw err
  })
}

忽略请求

相较于取消请求,忽略请求更为通用。我们只需要关注我们最后一次请求的结果,如果某次请求的返回结果并不是最新的,那么我们就忽略掉这个请求。忽略请求的一个精髓在于终止Promise的响应,这也是字节面试官经常问到的一个问题,我们需要做的就是返回一个Pending的Promise,从而做到终止的效果

使用锁标记

这里的锁只的是某个唯一标识,通过这个标识来对比当前请求是否过期,如下文的prevTimestamp

我们通过闭包,实现对prevTimestamp的缓存,从而做到对每次请求的返回进行对比的效果

typescript 复制代码
/**
 * 处理竞态请求
 *
 * @export
 * @param {(...params: any[]) => Promise<any>} fn
 * @return {*}
 */
export default function useFetch<T, P>(fn: (...params: [P, ...any[]]) => Promise<T>) {
  let prevTimestamp
  return function (params: P, ...rest: any[]): Promise<T> {
    return new Promise((resolve, reject) => {
      const curTimestamp = prevTimestamp = Date.now()
      const context = this
      fn.call(context, params, ...rest)
        .then(res => {
          // 只处理最新请求的返回
          if (curTimestamp === prevTimestamp) {
            resolve(res)
          }
        }).catch(err => {
          // 只处理最后一次请求的异常
          if (curTimestamp === prevTimestamp) {
            reject(err)
          }
        })
    })
  }
}

在使用的时候,我们就可以用这个方法把真正要发出的请求包一下(针对某一个具体的原子请求,从而做到多请求皆可竟态处理)

typescript 复制代码
import Axios from 'axios'

const _getSomeResource = (params: GetSomeResourceReq) => {
  return Axios.post('/xxx', params)
}

export getSomeResource = useFetch(_getSomeResource)

使用队列

与使用锁类似,我们把每个请求都放进队列里,对每次返回的请求进行判断,如果这个请求不是最新的请求,那么就忽略掉

typescript 复制代码
/**
 * 处理竞态请求
 *
 * @export
 * @param {(...params: any[]) => Promise<any>} fn
 * @return {*}
 */
export default function useFetch<T, P>(fn: (...params: [P, ...any[]]) => Promise<T>) {
  const queue = []
  return function (params: P, ...rest: any[]): Promise<T> {
    const p = new Promise((resolve, reject) => {
      const context = this
      fn.call(context, params, ...rest)
        .then(res => {
          const isLatest = queue[queue.length - 1] === p
          // 只处理最新请求的返回
          if (isLatest) {
            resolve(res)
          }
        }).catch(err => {
          const isLatest = queue[queue.length - 1] === p
          // 只处理最后一次请求的异常
          if (isLatest) {
            reject(err)
          }
        }).finally(() => {
           const idx = queue.findIndex(item => item === p)
           queue.splice(idx, 1)
        })
    })
    queue.push(p)
    return p
  }
}

总结

  • 为了解决前端开发中遇到的竟态请求问题,我们提供了两种解决方案:取消请求 & 忽略请求
  • 这两种方案都有一定的优劣,取消请求会导致客户端主动断开连接,可能对后台异常监控带来影响;忽略请求可能导致前端请求过于频繁,增加后台服务器压力,可以结合截流/防抖机制加以优化

参考

相关推荐
gnip1 天前
链式调用和延迟执行
前端·javascript
SoaringHeart1 天前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.1 天前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu1 天前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss1 天前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师1 天前
React面试题
前端·javascript·react.js
木兮xg1 天前
react基础篇
前端·react.js·前端框架
ssshooter1 天前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘1 天前
HTML--最简的二级菜单页面
前端·html
yume_sibai1 天前
HTML HTML基础(4)
前端·html