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

共 7487字,需浏览 15分钟

 ·

2023-10-16 02:46


点击上方  前端阳光 ,关注公众号

回复 加群 ,加入技术交流群交流群

a42cb68bcdc02a480f7e143fb0b5ea50.webp


前言

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

举个🌰

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

df26914ddd0a33641449836cd095f394.webp

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

9b22079c9612181ab9134750e618c190.webp

解决方案

取消请求

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

XMLHttpRequest 取消请求

XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。

如果请求已被发出,可以使用 abort() 方法立刻中止请求。

      
      const xhr= new XMLHttpRequest();

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

Fetch API 取消请求

fetch 号称是 AJAX 的替代品,出现于 ES6,它也可以发出类似 XMLHttpRequest 的网络请求。

主要的区别在于 fetch 使用了 Promise,要中止 fetch 发出的请求,需要使用 AbortController。

      
      const controller = new AbortController();
const signal = controller.signal;

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

Axios cancel Token 取消请求

相比原生 API,大多项目都会选择 axios 进行请求。

      
      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的缓存,从而做到对每次请求的返回进行对比的效果

      
      
/**
 * 处理竞态请求
 *
 * @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)
          }
        })
    })
  }
}

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

      
      import Axios from 'axios'

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

export const getSomeResource = useFetch(_getSomeResource)

使用队列

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

      
      
/**
 * 处理竞态请求
 *
 * @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 && !p.expired) {
            resolve(res)
          } else {
              p.expired = true // 避免后面的请求出列后,老的请求继续执行
          }
        }).catch(err => {
          const isLatest = queue[queue.length - 1] === p
          // 只处理最后一次请求的异常
          if (isLatest && !p.expired) {
            reject(err)
          } else {
            p.expired = true // 避免后面的请求出列后,老的请求继续执行
          }
        }).finally(() => {
           const idx = queue.findIndex(item => item === p)
           queue.splice(idx, 1)
        })
    })
    queue.push(p)
    return p
  }
}

总结

为了解决前端开发中遇到的竟态请求问题,我们提供了两种解决方案:取消请求 & 忽略请求

这两种方案都有一定的优劣,取消请求会导致客户端主动断开连接,可能对后台异常监控带来影响;忽略请求可能导致前端请求过于频繁,增加后台服务器压力,可以结合截流/防抖机制加以优化

作者:WeilinerL 链接:https://juejin.cn/post/7280740005567332404


a8f98047c94c59e4c67f7f9b9a75a4cd.webp

往期推荐


优秀文章汇总:https://github.com/Sunny-lucking/blog
技术交流群


我组建了技术交流群,里面有很多 大佬,欢迎进来交流、学习、共建。回复 加群 即可。 后台回复「 电子书 」即可免费获取 27本 精选的前端电子书!



    “分享、点赞 在看” 支持一波👍


浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报