队列方式
原理:
- 按照请求发起顺序入队
- 通过
isProcessing
标志防止并行处理
ts
import axios, { AxiosRequestConfig } from 'axios';
interface QueuedRequest<T = any> {
params: T;
resolve: (value: unknown) => void;
config?: AxiosRequestConfig;
}
class RequestQueue<T = any> {
private queue: QueuedRequest<T>[] = [];
private isProcessing = false;
async add(params: T, config?: AxiosRequestConfig): Promise<any> {
return new Promise((resolve) => {
this.queue.push({ params, resolve, config });
// 请求入队时,判断当前是否有请求正在执行
if (!this.isProcessing) this.process();
});
}
private async process() {
this.isProcessing = true;
while (this.queue.length > 0) {
const { params, resolve, config } = this.queue.shift()!;
try {
const response = await axios({
method: 'GET',
url: '/api/data',
params,
...config
});
resolve({ data: response.data, originalParams: params });
} catch (error) {
resolve({ error, originalParams: params });
}
}
this.isProcessing = false;
}
}
export default new RequestQueue();
适用场景:
- 需要严格控制请求顺序(比如支付流程)
缺点:
- 但是这种方式有个缺陷,如果当前请求很费时间,后续还存在其他请求,会导致用户体验下降
当然也可能存在前面的请求失败了,则终止所有请求的场景,可以如下拓展:
tsx
import axios, { AxiosRequestConfig, CancelTokenSource } from 'axios';
class AtomicRequestQueue {
private queue: Array<{
task: () => Promise<any>;
config?: AxiosRequestConfig
}> = [];
private globalController: AbortController | null = null;
private isProcessing = false;
private hasFailed = false; // 全局失败标记
// 添加请求到队列
addRequest(task: () => Promise<any>, config?: AxiosRequestConfig) {
this.queue.push({ task, config });
if (!this.isProcessing) this.process();
}
// 处理队列
private async process() {
this.isProcessing = true;
this.globalController = new AbortController(); // 创建全局终止控制器
for (const request of this.queue) {
if (this.hasFailed) break; // 已有失败则终止
try {
// 注入全局终止信号
const res = await request.task();
console.log('请求成功:', res);
} catch (error) {
if (!axios.isCancel(error)) {
// 非主动取消的错误触发全局终止
this.hasFailed = true;
this.globalController.abort();
console.error('请求失败触发全局终止', error);
}
break;
}
}
this.resetQueue();
}
// 重置队列状态
private resetQueue() {
this.queue = [];
this.globalController = null;
this.isProcessing = false;
this.hasFailed = false;
}
// 外部终止接口
abortAll() {
this.globalController?.abort();
this.resetQueue();
}
}
// 全局单例
export const atomicQueue = new AtomicRequestQueue();
取消之前的请求
通过 axios 的 AbortController
取消过期请求
tsx
import { useEffect, useRef } from 'react';
import axios, { AxiosRequestConfig } from 'axios';
export function useCancelableRequest() {
const abortRef = useRef<AbortController>();
const fetchData = async (config: AxiosRequestConfig) => {
// 取消前一个未完成的请求
if (abortRef.current) {
abortRef.current.abort();
}
const controller = new AbortController();
abortRef.current = controller;
try {
const response = await axios({
...config,
signal: controller.signal
});
return response.data;
} catch (err) {
if (!axios.isCancel(err)) throw err;
}
};
useEffect(() => {
return () => abortRef.current?.abort();
}, []);
return fetchData;
}
使用场景:
- 适用于绝大部分场景,比如搜索框实时获取、连续分页加载等
缺点:
- 需要手动控制管理器
忽略过期响应
为每个请求分配一个唯一的标识符(例如,递增的序列号或时间戳),并在响应中包含该标识符。前端只处理具有最新标识符的响应,忽略过期的响应。
tsx
import { useState, useEffect, useRef } from 'react';
import axios, { AxiosRequestConfig } from 'axios';
export function useRaceSafeFetch() {
const versionRef = useRef(0);
const safeFetch = async <T,>(config: AxiosRequestConfig): Promise<T> => {
const currentVersion = ++versionRef.current;
try {
const response = await axios(config);
// 版本校验:只处理最新请求的响应
if (currentVersion === versionRef.current) {
return response.data;
}
throw new Error('EXPIRED_RESPONSE');
} catch (error) {
if (axios.isCancel(error) || error.message === 'EXPIRED_RESPONSE') {
return Promise.reject(new Error('请求已被更新'));
}
throw error;
}
};
useEffect(() => {
return () => {
versionRef.current = -1; // 组件卸载时标记所有请求过期
};
}, []);
return safeFetch;
}
使用场景:
- 适用于请求响应顺序不重要,只需要最新结果的场景
缺点:
- 需要维护key映射
选择哪种方法?
- 取消之前的请求:最常用,适合大多数场景,尤其是搜索实时响应、快速分页加载等
- 忽略过期响应:请求顺序不重要,只需要最新结果的场景
- 队列方式:适用于严格控制请求顺序的场景(比如支付流程)。但可能导致用户体验下降(请求需要排队)