请求竞态的定义
请求竞态通常发生在短时间内连续触发多个异步请求(例如,用户在搜索框中快速输入、快速点击分页按钮等),并且这些请求的响应返回顺序与发送顺序不一致时。如果应用程序的状态依赖于最新请求的结果,但一个较早发出的请求的响应却在较新请求的响应之后到达,就可能导致界面显示了过时或错误的数据。
请求竞态问题场景
核心问题场景示例:
假设有一个搜索框,用户每输入一个字符,就向后端发送一个搜索请求。
- 用户输入 "a",发送请求
req1(搜索 "a")。 - 用户快速输入 "b",发送请求
req2(搜索 "ab")。 - 用户快速输入 "c",发送请求
req3(搜索 "abc")。
网络延迟是不可预测的:
- 可能
req3最先返回。 - 然后
req1返回。 - 最后
req2返回。
如果每次请求返回都直接更新搜索结果列表,那么最终界面显示的是 req2 (搜索 "ab") 的结果,而用户期望看到的是 req3 (搜索 "abc") 的结果,这就是竞态问题。
解决方案
-
- 取消先前未完成的请求:当新的请求发出时,主动取消掉还在进行中的、由同一操作触发的旧请求。
-
- 忽略过时的请求响应:为每个请求设置标识,只处理与最新发出的请求标识相符的响应。
忽略过时的请求响应的原理:【给请求计数,通过对比判断出最后一个请求】
连续请求过程中,每当发出一个请求,就将之前正在pending的请求的Promise reject掉,并且该请求的XHR对象执行abort()
递增的计数器
- 定义一个计数器requestVersion,每次发起请求时递增(const currentVersion = ++ requestVersion)
- 每次发起请求前,保存当前的currentVersion
- 响应验证:当响应返回时,检查currentVersion是否等于最新的requestVersion
- 相等,说明是最新结果,可以显示
- 不相等,说明已有更新的请求发出,忽略此结果
取消请求:对于使用fetch请求的,可以使用AbortController解决
创建AbortController实例,fetch请求携带参数signal,使用abortController.abort()终止请求。
useEffect(()=>{
const abortController = new AbortController();
fetch(`https://a.com/${id}`,{signal:abortController.signal})
.then()
return () => {
abortController.abort();
}
})
取消请求:对于使用axios请求的,使用cancelToken
const source = axios.CancelToken.source();
axios.get('/xxx', {
cancelToken: source.token
}).then(function (response) {
// ...
});
source.cancel() // 取消请求