游戏攻略

如何避免旧请求的数据覆盖掉最新请求

栏目:游戏攻略 日期: 作者:游戏资讯

在网络检索的情境中,我们经常会对同一个接口发起不同检索条件的请求。然而,如果前一个请求的响应较慢,就可能会导致后续请求的结果被覆盖。

举例来说,我们首先发起一个搜索请求,参数为A;这个请求还未结束时,我们又发起了参数为B的搜索请求。由于网络或后端服务处理等原因,参数B的请求可能会先得到响应,然后我们将其数据展示在页面上。随后,参数A的搜索请求也返回了结果。然而,实际上,参数B的响应结果才是我们需要展示的最新数据。

那么,我们该如何避免这种现象呢?

请求锁定

我们可以对发起请求的按钮、输入框等元素,或者在全局范围内添加loading状态。只有在上一个请求的响应结果返回后,才取消loading状态,允许用户发起下一次请求。

const App = () => {
  const [loading, setLoading] = useState(false);

  const request = async (data) => {
    if (loading) {
      // 若请求还未结束,则无法发起新的请求
      return;
    }
    setLoading(true);
    const result = await axios("/api", { data, method: "post" });
    setLoading(false);
  };

  return (
    <div className="app">
      <Form disabled={loading} onFinish={request}>
        <Form.Item>
          <Input />
        </Form.Item>
        <Button htmlType="submit" loading={loading}>
          搜索
        </Button>
      </Form>
    </div>
  );
};

通过直接对请求进行控制,我们根本不会出现多个请求并行的情况,因此也就不会出现覆盖的问题。

防抖

在纯输入框的搜索功能中,一般会在用户停止输入一段时间后才发起搜索,这样可以增加两次检索请求之间的时间间隔。

const App = () => {
  const request = async () => {};

  return (
    <div className="app">
      {/* 停止输入700ms后触发请求 */}
      <input onInput={debounce(request, 700)} />
    </div>
  );
};

然而,防抖措施并不能完全杜绝数据被覆盖。如果上一次的请求确实很慢,那么仍可能出现覆盖后续请求的情况。

取消上次的请求

当需要发起新的请求,而上次的请求尚未结束时,可以取消上次的请求。

const App = () => {
  const cancelSouceRef = useRef(null);

  const request = async () => {
    if (cancelSouceRef.current) {
      // 若存在上次的请求尚未结束,则手动取消
      cancelSouceRef.current.cancel("手动取消上次的请求");
    }
    const source = axios.CancelToken.source();
    cancelSouceRef.current = source;

    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      setData(response.data);
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log("请求被取消", error.message);
      } else {
        console.log("请求出错", error.message);
      }
    } finally {
      cancelSouceRef.current = null;
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

需要注意的是,如果服务端已接收到了请求,不论前端是否取消了请求,服务端都会完整查询并返回,只是浏览器不再处理而已。

时序控制

每次请求时,我们都会为该请求分配一个唯一标识,并在该组件的全局范围内保存最新的标识。如果响应时的标识表示最新的标识,那么我们就不处理该响应的结果。

标识只需要在当前组件中是唯一的,比如自增数字、时间戳、随机数等都可以。

const App = () => {
  const requestIdRef = useRef(0); // 保存最新的请求id

  const request = async () => {
    requestIdRef.current++; // 每次自增

    const curRequestId = requestIdRef.current;
    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      if (curRequestId < requestIdRef.current) {
        // 当前请求不是最新的,不做任何处理
        return;
      }
      setData(response.data);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

这是一种比较简单有效的方案,同时也能让用户任意搜索。

总结

在实际应用中,通常会采用多种方案的组合。例如,在纯输入框触发搜索的场景中,一般会采用防抖和时序控制的两种方案的组合,既能减少触发请求的次数,又能避免数据的相互覆盖。

有些人可能会想到“控制请求的并发数量”,使用队列、递归等方式,将每次发起的请求放到队列的后面,然后按照队列的顺序发起请求。我们之前在文章《JavaScript 中的 Promise 异步并发控制》中也探讨过这种场景。

这种方式确实能够解决问题,但有一种“杀鸡用牛刀”的感觉。因为在当前场景中,对同一个接口发起多次请求时,我们更关心的是最新请求的结果,之前请求的结果可以直接丢弃。

欢迎关注我的公众号:前端小茶馆。

关键词:

相关资讯