前端如何解决竞态问题

What什么是竞态问题

它旨在描述一个系统或者进程的输出 依赖于 不受控制的事件执行顺序。这个词语来源于两个信号彼此竞争、影响谁先输出

Why为什么会出现竞态问题

具体到前端开发中,竞态问题出现的原因是:我们无法保证异步请求的完成顺序 严格等于它们的开始顺序

举个例子:用户在搜索框输入关键词进行搜索,若短时间内连续键入两次,第二次的搜索结果比第一次的先返回

在开发者没有实现并发控制的前提下,最终展示的就是第一次搜索的结果,不符合用户的预期

How如何解决竞态问题

加锁

竞态问题的本质是多个请求同时进行,无法保证谁先结束

那么最简单粗暴的方式就是:在一个请求完成前,不允许发起新的请求

可以通过给触发元素或者请求函数加锁,来实现上述的效果

  1. 给触发元素加锁

维护loading状态,设置按钮在loading=true时禁用

TypeScript 复制代码
function App() {
  const [loading, setLoading] = useState(false)
  
  // 发送异步请求
  async function getData() {
    setLoading(true)
    const resp = await axios.get('/xxx')
    setLoading(false)
  }
  
  function handleClick() {
    getData()
  }
  
  return (
    <button disabled={loading} onClick={handleClick}>发送请求</div>
  )
}
  1. 给异步请求函数加锁

使用ahooks的useLockFn包裹异步请求函数,在函数正在执行时,再调用函数就会直接return

TypeScript 复制代码
import { useLockFn } from 'ahooks'

function App() {  
  // 发送异步请求
  const getData = useLockFn(async () => {
    const resp = await axios.get('/xxx')
  })
  
  function handleClick() {
    getData()
  }
  
  return (
    <button onClick={handleClick}>发送请求</div>
  )
}

取消过期请求

在发起新的请求之前,取消正在进行的请求

  1. 使用AbortController

AbortController是浏览器内置的API,用于构造一个controller实例

常见的请求库(fetch、axios>0.22.0)都已支持通过AbortController取消请求

使用方法也很简单:将controller.signal传入请求函数中,将signal与请求关联起来,在需要的时候调用controller.abort()去取消这个请求

TypeScript 复制代码
const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});

// 取消请求
controller.abort()

我们也可以将生成controller实例的逻辑封装成hook,简化使用,详见如何使用AbortController取消请求 - 掘金

TypeScript 复制代码
import { useUnmount } from 'ahooks';
import { useRef } from 'react';

export function useAbortController() {
  const controller = useRef<AbortController>(new AbortController());

  useUnmount(() => {
    controller.current.abort();
  });

  return {
    signal: controller.current.signal,
    abort: controller.current.abort,
  };
}
  1. 使用第三方库awesome-imperative-promise

awesome-imperative-promise实现了指令式的promise,支持在promise外部手动调用resolve/reject/cancal等指令

我们可以在每次调用请求函数前,先调用一次cancel方法取消正在进行的请求

TypeScript 复制代码
import { createImperativePromise } from 'awesome-imperative-promise';

function App() {  
  // 发送异步请求
  const getData = async () => {
    const resp = await axios.get('/xxx')
  }
  
  const { cancel } = createImperativePromise(getData)
  
  function handleClick() {
    cancel()
    getData()
  }
  
  return (
    <button onClick={handleClick}>发送请求</div>
  )
}

忽略过期请求

允许多个请求同时进行,但只处理最后发起的请求的结果

实现思路是:

  • 利用变量latest记录最新一次请求的时间戳
  • 在发出请求时,使用当前时间戳标记这次请求的id
  • 在请求结束后,当且仅当id取值与latest相等时,才更新数据/视图
TypeScript 复制代码
import dayjs from 'dayjs'

function App() {  
  const latest = useRef<string>('')
  
  // 发送异步请求
  const getData = async () => {
    // 当前时间戳(毫秒)
    const id = String(dayjs().unix() * 1000)
    latest.current = id
    const resp = await axios.get('/xxx')
    if (id === latest.current) {
      // 更新数据/视图
    }
  }
  
  function handleClick() {
    getData()
  }
  
  return (
    <button onClick={handleClick}>发送请求</div>
  )
}

总结

在前端常见的搜索、分页切换等场景中,由于我们无法保证异步请求的完成顺序 严格等于它们的开始顺序,容易出现竞态问题、导致页面展示的内容不符合预期

要解决竞态问题,可以通过加锁、取消过期请求 或 忽略过期请求的方式

  • 加锁:在请求完成前、不允许发出新的请求

    • 优点:实现简单直接
    • 缺点:阻塞用户操作,可能造成用户等待时间较长
  • 取消过期请求:在发起新的请求前,取消正在进行的请求

    • 优点:如果请求被取消时还没有到达服务端,可以减轻服务端的压力
    • 缺点:依赖API或者第三方库
  • 忽略过期请求:允许多个请求同时进行,但只处理最后发起的请求的结果

    • 优点:实现更通用,不依赖API或第三方库

    • 缺点:如果短时间内多次触发,可能造成服务端处理压力较大

参考资料

相关推荐
审小匠OpenCPAi4 分钟前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
Revolution617 分钟前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化
腻害兔23 分钟前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
whyfail31 分钟前
前端学 Spring Boot(2):一次点击,如何穿过整个后端?
前端·spring boot·后端
AI多Agent协作实战派1 小时前
AI多Agent协作系统实战(二十三):Agent读HEARTBEAT.md不读AGENTS.md——openclaw的文件加载之谜
前端·数据库·人工智能·uni-app
2601_955760072 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎
KaMeidebaby2 小时前
卡梅德生物技术快报|bli亲和力检测gst:告别批量跑胶:BLI实时酶切监测技术加速GST融合蛋白下游流程优化
前端·网络·数据库·人工智能·算法
bloglin999992 小时前
langchain 和 langgraph 和 react
javascript·react.js·langchain
触底反弹3 小时前
🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
前端·javascript·react.js
耳东小鹿4 小时前
对象常用方法
前端