竞态问题
在一些频繁请求的接口上,如列表快速翻页、查询表单快速点击查询时都可能出现。下面模拟了这种情况:
延时请求会在1s后返回,ID区分立刻返回,可以看到由于延时请求比ID区分到达更晚,导致数据与期望的不一致。
虽然可以通过一些控制查询按钮使能状态等方法规避,但是也只是治标不治本,下面分享两个针对这个问题的处理方法,欢迎交流~
方案1:通过ID区分
最常见的解法莫过于为每个请求设置id,在数据消费者拿到数据后对比请求前后id,区分是否为本次请求的数据。
下面是通过fetch封装的一个支持请求id的方法。
export interface RequestProps<T = any, R = any> {
url: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
data?: T;
params?: R;
requestId?: string;
}
const baseUrl = "http://localhost:7001";
const request = async ({
url,
method = "GET",
data,
params,
requestId,
}: RequestProps) => {
const searchParams = new URLSearchParams(params).toString();
const urlWithParams = `${baseUrl}${url}?${searchParams}`;
const res = await fetch(urlWithParams, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await res.json();
return { data: result, requestId: requestId };
};
export default request;
在使用时传入requestId参数:
const currentRequestId = useRef<string>("");
const handleData = async (type: string) => {
currentRequestId.current = crypto.randomUUID();
const { data, requestId } = await request({
url: "/api/data",
method: "GET",
params: { type },
requestId: currentRequestId.current,
});
if (requestId === currentRequestId.current) {
setData(data.data);
}
};
效果如下:
方案2:自动取消
在实际生产中,可以看到上述的方案存在诸多弊端:
- 请求没有取消,依旧正常发送、返回,浪费带宽
- 每次需要在业务逻辑中处理请求的问题,不够解耦
- 使用不方便,在不同的模块需要写同样的处理逻辑(可以通过封装解决)
下面分享一种简单优雅的处理方法,代码如下:
const requestMap = new Map<string, AbortController>();
const key = url + method;
if (autoCancel && requestMap.has(key)) {
requestMap.get(key)?.abort();
requestMap.delete(key);
}
const controller = new AbortController();
if (autoCancel) {
requestMap.set(key, controller);
}
const res = await fetch(urlWithParams, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: controller.signal,
});
通过一个autoCancel参数控制是否支持自动取消,解决了上述的几点痛点问题。
可能有疑惑的点在于:为什么key要这样组合呢?
结合业务场景,
- 一般的会出现竞态问题的地方都是同一个接口的快速重复调用,因此,直接通过
url可以初步作为是否需要支持自动取消的key。 - 考虑到
RESTFUL风格的API形式,需要加上请求的method再做区分。
实现的效果如下:
可以看到在后续触发相同的请求时,自动取消了之前的请求。
补充
采用第二种方案时有一个点需要注意:当使用try...catch处理异常时,请求取消也会被catch到,因此需要单独加一个判断处理:
......
} catch (error: any) {
if (error.name === "AbortError") {
return null;
}
console.error(error);
}
......
完整代码:
export interface RequestProps<T = any, R = any> {
url: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
data?: T;
params?: R;
requestId?: string;
autoCancel?: boolean;
}
const baseUrl = "http://localhost:7001";
const requestMap = new Map<string, AbortController>();
const request = async ({
url,
method = "GET",
data,
params,
requestId,
autoCancel = false,
}: RequestProps) => {
const key = url + method;
if (autoCancel && requestMap.has(key)) {
requestMap.get(key)?.abort();
requestMap.delete(key);
}
const controller = new AbortController();
if (autoCancel) {
requestMap.set(key, controller);
}
const searchParams = new URLSearchParams(params).toString();
const urlWithParams = `${baseUrl}${url}?${searchParams}`;
let result = null;
try {
const res = await fetch(urlWithParams, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: controller.signal,
});
result = await res.json();
} catch (error: any) {
if (error.name === "AbortError") {
return null;
}
console.error(error);
}
return { data: result, requestId: requestId };
};
export default request;