在现代 Web 应用中,异步数据请求是非常常见的操作。然而,当多个请求同时进行时,可能会出现数据竞争的问题,导致用户界面显示不正确的数据。在这篇文章中,我们将探讨如何在前端开发中解决这一问题,并以 React 和 Ant Design 为例进行说明。
问题描述
假设我们有一个表格组件,用于显示从服务器获取的数据。用户可以通过点击不同的按钮来触发不同的数据请求:
- 点击按钮1:请求数据1,3秒后返回。
- 点击按钮2:请求数据2,1秒后返回。
理想情况下,表格应该显示最后一次请求的数据。然而,由于请求1的返回时间较长(3秒),如果用户在请求1未完成时又触发了请求2,表格可能会先显示请求2的数据,然后在请求1返回后又被覆盖为请求1的数据。这种情况显然是不正确的,因为用户期望看到的是最新请求的数据。
解决方案
为了解决这个问题,我们需要确保在新的请求发起时,之前未完成的请求不会影响当前请求的结果。我们可以通过创建一个可取消的异步任务来实现这一点。
解决方案的原理
createCancelTask
函数的核心思想是通过一个可变的 cancel
方法来控制异步任务的执行状态。以下是详细的实现步骤和解释:
-
初始状态:
cancel
是一个空方法,初始时没有任何副作用。- 这意味着在第一次调用时,
cancel
不会对任何东西产生影响。
-
发起请求:
- 每次发起新的请求时,首先调用
cancel()
方法。 - 这一步确保了上一次请求的
resolve
和reject
被清空。 - 然后,
cancel
被重新赋值为一个新方法,该方法将resolve
和reject
赋值为空函数。
- 每次发起新的请求时,首先调用
-
请求返回:
- 当请求返回时,如果
resolve
和reject
已被清空(即调用了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
函数,我们有效地解决了异步数据请求中的竞争问题。这种方法确保了用户界面始终显示最新请求的数据,避免了由于请求返回顺序不一致而导致的数据错误显示。这种解决方案不仅适用于本示例,也可以推广到其他需要处理异步请求的场景中。希望这篇文章对你有所帮助!