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

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

相关推荐
前端不能无几秒前
ES6 新增的Proxy与Reflect详解与妙用
前端·javascript
onejason2 分钟前
使用Python爬虫获取淘宝App商品详情
前端·python
rnil5 分钟前
开发浏览器插件-基于配置自动化执行
前端·浏览器
多看书少吃饭9 分钟前
WebRTC简介及应用
前端·vue.js·websocket·webrtc
冬冬小圆帽1 小时前
Svelte 深度理解
前端·javascript·vue.js
罗婕斯特1 小时前
Scala中while和for循环
java·开发语言·前端
小二·1 小时前
如何在Vue项目中封装axios
前端·javascript·vue.js
Billy Qin2 小时前
【Mac】npm error Error: EACCES: permission denied, mkdir‘/Users/...
前端·macos·npm
好青崧2 小时前
HTML元素小卖部:表单元素 vs 表格元素选购指南
前端
饼干帅成渣2 小时前
HTML跑酷
前端·html