请求竞态问题

74 阅读2分钟

请求竞态的定义

请求竞态通常发生在短时间内连续触发多个异步请求(例如,用户在搜索框中快速输入、快速点击分页按钮等),并且这些请求的响应返回顺序与发送顺序不一致时。如果应用程序的状态依赖于最新请求的结果,但一个较早发出的请求的响应却在较新请求的响应之后到达,就可能导致界面显示了过时或错误的数据。

请求竞态问题场景

核心问题场景示例:

假设有一个搜索框,用户每输入一个字符,就向后端发送一个搜索请求。

  1. 用户输入 "a",发送请求 req1 (搜索 "a")。
  2. 用户快速输入 "b",发送请求 req2 (搜索 "ab")。
  3. 用户快速输入 "c",发送请求 req3 (搜索 "abc")。

网络延迟是不可预测的:

  • 可能 req3 最先返回。
  • 然后 req1 返回。
  • 最后 req2 返回。

如果每次请求返回都直接更新搜索结果列表,那么最终界面显示的是 req2 (搜索 "ab") 的结果,而用户期望看到的是 req3 (搜索 "abc") 的结果,这就是竞态问题。

解决方案

    1. 取消先前未完成的请求:当新的请求发出时,主动取消掉还在进行中的、由同一操作触发的旧请求。
    1. 忽略过时的请求响应:为每个请求设置标识,只处理与最新发出的请求标识相符的响应。

忽略过时的请求响应的原理:【给请求计数,通过对比判断出最后一个请求】

思路:(juejin.cn/post/684490…)

连续请求过程中,每当发出一个请求,就将之前正在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() // 取消请求