如何避免舊請求的資料覆蓋掉最新請求

前端小茶馆發表於2024-09-14

我的部落格地址:如何避免舊請求的資料覆蓋掉最新請求 - 蚊子的前端部落格

在檢索的場景中,經常會對同一個介面發起不同的檢索條件的請求,若前一個請求響應較慢時,可能會覆蓋掉我們後發起請求的結果。

如我們先發起一個搜尋請求,引數是 A;這個請求還沒結束,我們發起了引數是 B 的搜尋請求;可能因網路原因或者後端服務處理等原因,後發起的引數 B 的請求先響應了,然後我們把資料展示到頁面中;過一會兒之前發起引數是 A 的搜尋請求也返回結果了。但實際上,引數 B 的響應結果,才是我們需要展示的最新的資料。

那麼如何避免這種現象呢?

1. 請求鎖定

可以對發起請求的按鈕、輸入框等,或者是在全域性中,新增 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>
  );
};

直接用源頭進行控制,根本不存在多個請求並行的情況,也就無所謂誰覆蓋誰了。

2. 防抖

一般應用在純輸入框的搜尋功能中,在使用者停止輸入一段時間後,才發起搜尋,可以加大兩次檢索請求之間的時間間隔.

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

  return (
    <div className="app">
      {/* 停止輸入700ms後觸發請求 */}
      <input onInput={debounce(request, 700)} />
    </div>
  );
};

防抖措施並不能完全杜絕資料被覆蓋。假如上一次的請求確實很慢,那也會出現覆蓋後續請求的現象。

3. 取消上次的請求

當需要發起新的請求,上次的請求還沒結束時,可以取消上次請求。

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>
  );
};

如果服務端已接收到了請求,不論前端是否取消了請求,服務端都會完整查詢並返回,只是瀏覽器不再處理而已。

4. 時序控制

我們在每次請求時,都給該請求一個唯一標識,並在該元件的全域性中儲存最新的標識,若響應時的標識表示最新的標識,則不處理該響應的結果。

標識只要在當前元件中唯一即可,如自增數字、時間戳、隨機數等,都可以。

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>
  );
};

這是一種比較簡單有效,同時能讓使用者任意搜尋的方案。

5. 總結

當然,在實際中,肯定也是多方案的組合。比如純輸入框觸發搜尋的場景中,一般是防抖+時序控制的兩種方案的組合,既能減少觸發請求的次數,又能避免資料的相互覆蓋。

有的同學可能想到「控制請求的併發數量」,用佇列遞迴等方式,每次將發起的請求都放到佇列的後面,然後按照佇列的順序發起請求。如我們之前曾經在文章 JavaScript 中的 Promise 非同步併發控制 探討過這種場景。

這種方式倒也能解決問題,不過有種「殺雞用牛刀」的感覺。因為在現在的場景中,對同一個介面發起多次請求時,其實我們更關心的是最新請求的結果,之前請求的結果直接可以扔掉了。

歡迎關注我的公眾號:前端小茶館。

前端小茶館

相關文章