一分钟知识点:解决前端常见异步数据竞态问题

369 阅读4分钟

什么是异步数据竞态问题

异步数据竞态问题多发生在多个异步操作并发执行,而后续操作依赖于这些操作的执行顺序或执行结果时,可能导致预期之外的错误或行为

问题表现

  • 先后顺序不确定:展示顺序错乱
  • 数据更新混乱:比如一个请求先返回,但后来的请求覆盖了它的数据
  • UI卡顿或者闪烁:如果多个请求并发执行而没有控制,可能导致页面多次重新渲染

典型问题场景

  1. 快速切换选项卡 异步数据竞态问题.gif

  2. 搜索框输入联想

这里我用随机数模拟了一下请求的时长,可以看到当我输入“app”的时候结果还算正常,但是当我删除到“a” 的时候,结果展示的是“ap”的搜索结果

搜索数据竞态问题.gif

避免异步数据竞态问题的方法

异步数据竞态问题通常发生在并发请求中,避免竞态问题的关键在于:合理的请求顺序、并发控制以及对异步结果的正确管理

取消过期请求

实现方式

  1. 使用 AbortController(现代浏览器原生支持)
let controller;

async function fetchData(query) {
  if (controller) controller.abort(); // 取消前一个请求
  controller = new AbortController();
  try {
    const res = await fetch(`/api?q=${query}`, {
      signal: controller.signal
    });
    // 处理数据
  } catch (err) {
    if (err.name === 'AbortError') return; // 忽略取消错误
  }
}
  1. 第三方库如 axios 的 cancelToken

适用场景

用户频繁触发异步操作,且只需保留最新结果的场景

典型用例

  1. 搜索框输入联想

    • 用户连续输入时(如输入 "a" → "ap" → "app"),取消之前的请求,只保留最后一次输入的请求结果。
    • 避免旧搜索词(如 "a")的响应覆盖新搜索词(如 "app")的结果。
  2. 选项卡切换

    • 用户快速切换选项卡时(如 Tab1 → Tab2 → Tab3),取消之前的 Tab1、Tab2 的请求,确保只展示当前选中 Tab3 的数据。
  3. 表单防重复提交

    • 用户多次点击提交按钮时,取消前一次未完成的请求,确保只提交最后一次操作。(这里其实更推荐做个防抖)

忽略过期响应

这种方法兼容性强,无需依赖特定 API,适用于任何环境,但是过期请求仍在后台运行

实现方式

  1. 使用requestId控制
let requestId = 0;

async function loadData() {
  const currentId = ++requestId;
  const data = await fetchData();
  if (currentId !== requestId) return; // 忽略过期响应
  render(data);
}
  1. 最近看到一个大佬分享的忽略过期响应的很牛的方法,核心如下
function createCancelTask(asyncTask){
    let cancel = () => {}
    return (...args) => {
        return new Promise((resolve,reject)=>{
            cancel()
            cancel = () => {
                resolve = reject = () => {}
            }
            asyncTask(...args).then(
                (res) => resolve(res),
                (err) => reject(err)
            )
        })
    }
}

适用场景

无法取消请求(如兼容性限制或请求已发送到后端)或者需要简单实现,不依赖复杂 API 或第三方库的场景。

典型用例

  1. 兼容性要求高的环境

    • 旧版本浏览器不支持 AbortController 时,通过唯一标识(如时间戳、序列号)标记请求,仅处理最新标识对应的响应。
  2. 竞态风险较低的场景

    • 如页面初始化时加载多个并行请求,但只需确保最终显示正确数据(例如:仪表盘中的多个统计卡片)。

控制请求顺序

实现方式

这个方法的核心是做一把锁

let isLocked = false;

async function sequentialRequest(data) {
  if (isLocked) return;
  isLocked = true;
  await sendData(data);
  isLocked = false;
}

适用场景

需要严格保证异步操作顺序**,避免并发冲突,或者请求依赖前一次请求结果的场景。

典型用例

  1. 分步表单提交
    • 用户分步骤填写表单(如注册流程),每一步提交依赖前一步的结果,需确保请求按顺序执行
  2. 实时数据同步
    • 例如聊天应用,需要保证消息发送的顺序与服务器确认顺序一致,避免消息错乱。
  3. 防抖与节流
    • 合并高频操作(如窗口缩放事件),确保一段时间内只执行最后一次请求。

快去你的项目看看有没有类似的问题吧,或者有什么更好的解决方法欢迎评论区讨论