解决前端异步请求中的数据竞争问题

在现代 Web 应用中,异步数据请求是非常常见的操作。然而,当多个请求同时进行时,可能会出现数据竞争的问题,导致用户界面显示不正确的数据。在这篇文章中,我们将探讨如何在前端开发中解决这一问题,并以 React 和 Ant Design 为例进行说明。

问题描述

假设我们有一个表格组件,用于显示从服务器获取的数据。用户可以通过点击不同的按钮来触发不同的数据请求:

  1. 点击按钮1:请求数据1,3秒后返回。
  2. 点击按钮2:请求数据2,1秒后返回。

理想情况下,表格应该显示最后一次请求的数据。然而,由于请求1的返回时间较长(3秒),如果用户在请求1未完成时又触发了请求2,表格可能会先显示请求2的数据,然后在请求1返回后又被覆盖为请求1的数据。这种情况显然是不正确的,因为用户期望看到的是最新请求的数据。

解决方案

为了解决这个问题,我们需要确保在新的请求发起时,之前未完成的请求不会影响当前请求的结果。我们可以通过创建一个可取消的异步任务来实现这一点。

解决方案的原理

createCancelTask 函数的核心思想是通过一个可变的 cancel 方法来控制异步任务的执行状态。以下是详细的实现步骤和解释:

  1. 初始状态

    • cancel 是一个空方法,初始时没有任何副作用。
    • 这意味着在第一次调用时,cancel 不会对任何东西产生影响。
  2. 发起请求

    • 每次发起新的请求时,首先调用 cancel() 方法。
    • 这一步确保了上一次请求的 resolvereject 被清空。
    • 然后,cancel 被重新赋值为一个新方法,该方法将 resolvereject 赋值为空函数。
  3. 请求返回

    • 当请求返回时,如果 resolvereject 已被清空(即调用了 cancel),则不会对当前状态产生影响。
    • 这确保了只有最后一次请求的结果会被应用到界面上。

核心原理其实就是把上次的resolve和reject变成空函数,这样当上次请求回来,他的状态就一直是pending了

代码实现

以下是 createCancelTask 函数的实现代码:

typescript 复制代码
type TAsyncTask<T extends any[], R> = (...args: T) => Promise<R>;

const createCancelTask = <T extends any[], R>(asyncTask: TAsyncTask<T, R>) => {
  let cancel = () => {};

  return (...args: T): Promise<R> => {
    return new Promise((resolve, reject) => {
      cancel(); // 清空上一次请求的 resolve 和 reject
      cancel = () => {
        reject = resolve = () => {}; // 将当前请求的 resolve 和 reject 置为空函数
      };

      asyncTask(...args).then(
        (res) => resolve(res), // 如果请求成功,调用 resolve
        (err) => reject(err),  // 如果请求失败,调用 reject
      );
    });
  };
};

使用示例

在我们的示例中,我们使用 createCancelTask 来包装一个模拟的异步数据请求函数 fetchData。这个函数根据不同的按钮点击事件,使用不同的超时时间和数据集:

typescript 复制代码
import type { TableProps } from 'antd';
import { Button, Space, Table } from 'antd';
import { useState } from 'react';

interface DataType {
  key: string;
  name: string;
}

const columns: TableProps<DataType>['columns'] = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    render: (text) => <a>{text}</a>,
  },
];

const data1: DataType[] = [
  {
    key: '1',
    name: '我是张三',
  },
];
const data2: DataType[] = [
  {
    key: '1',
    name: 'John Brown',
  },
];

const fetchData = createCancelTask(
  async (timeout = 1000, data: DataType[]) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(data); // 模拟异步请求返回数据
      }, timeout);
    });
  },
);

const onClick = async (type: 1 | 2) => {
  const data = type === 1 ? data1 : data2;
  const timeout = type === 1 ? 3000 : 1000;
  const res = await fetchData(timeout, data);

  setDataSource(res as DataType[]); // 更新表格的数据源
};

export default () => {
  const [dataSource, setDataSource] = useState<DataType[]>([]);

  return (
    <>
      <Space>
        <Button onClick={() => onClick(1)}>数据1</Button>
        <Button onClick={() => onClick(2)}>数据2</Button>
      </Space>
      <Table<DataType> columns={columns} dataSource={dataSource} />
    </>
  );
};

在这个示例中:

  • onClick 函数根据按钮类型决定使用哪个数据集和超时时间。
  • fetchData 函数通过 createCancelTask 包装,确保只有最后一次请求的结果会被应用到表格中。

总结

通过使用 createCancelTask 函数,我们有效地解决了异步数据请求中的竞争问题。这种方法确保了用户界面始终显示最新请求的数据,避免了由于请求返回顺序不一致而导致的数据错误显示。这种解决方案不仅适用于本示例,也可以推广到其他需要处理异步请求的场景中。希望这篇文章对你有所帮助!

相关推荐
却尘几秒前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare1 分钟前
浅浅看一下设计模式
前端
Lee川5 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix32 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人35 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl38 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅42 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust