如何在React中優雅的使用Interval(輪詢)

不羈的風發表於2022-05-12
在前端開發中,經常會使用輪詢(setInterval),比如後端非同步資料處理,需要定時查詢最新狀態。但是在用React Hook進行輪詢操作時,可能會發現setInterval沒有那麼輕鬆駕馭,今天筆者就來談談在專案開發中是如何解決setInterval呼叫問題的,以及如何更加優雅的使用setInterval

問題的引入

先從一個簡單的例子開始,為了便於敘述,本文中的案例用一個計數定時器來演示。

import React, { useEffect, useState } from "react";

export default function IntervalExp() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(timer);
  }, []);
  return (
    <div>
      <p>當前計數:{count}</p>
    </div>
  );
}

首先使用useState定義了一個count變數,然後在useEffect中,定義了一個名為timer的定時器,並在定時器中執行count+1操作,並在元件解除安裝時清除定時器。

理想狀態下,count會執行+1操作,並不斷的遞增。但實際並非如此,count在變為1以後,將不再有任何變化。原因很簡單,useEffect中由於沒有將依賴的count物件新增到依賴物件陣列中,所以它每次拿到的都是老的count物件,也就是0。

方法一:新增依賴陣列

import React, { useEffect, useState } from "react";

export default function IntervalExp() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    console.log("更新了", timer);
    return () => clearInterval(timer);
  }, [count]);
  return (
    <div>
      <p>當前計數{count}</p>
    </div>
  );
}

當把count物件加入到依賴陣列以後,可以發現定時器現在可以正常工作了。但是注意這裡有個坑,在return的時候,即元件解除安裝的時候,一定要做清理操作,否則你的定時器會執行的越來越快,因為新的定時器會不斷生成,但老的定時器卻沒有清理

但是這種方式完美嗎?並不然,如果定時器操作的資料包含父元件傳遞的props,或者是其他的state,都需要加到依賴陣列中,這樣做不僅不美觀,而且容易出錯。同時,這種方式還有個問題,就是定時器要在每次變化時要重新生成,這必然也會有很高的效能損耗

方法二:不新增依賴陣列的方式(useRef)

useRef是官方的hook,使用useRef定義的物件有個current物件,是可以儲存資料的,而且儲存的資料可以被修改,並在元件的每一次渲染中,都能從current中拿到最新的資料。基於ref的這一特性,實現一個名為useInterval的自定義hook。

import { useEffect, useRef } from "react";

export const useInterval = (cb: Function, time = 1000) => {
  const cbRef = useRef<Function>();
  useEffect(() => {
    cbRef.current = cb;
  });
  useEffect(() => {
    const callback = () => {
      cbRef.current?.();
    };
    const timer = setInterval(() => {
      callback();
    }, time);
    return () => clearInterval(timer);
  }, []);
};

在這個自定義hook中,有回撥函式和輪詢時間兩個引數。使用useEffect把最新的回撥函式賦值給ref.current,這樣在第二個useEffect中就能從ref.current上拿到最新的callback,然後在定時器中執行它。
在專案中如何使用呢?

useInterval(() => {
    setCount(count + 1);
  }, 1000);

只需引入自定義hook,並按照上面的格式呼叫即可。

方法三:更高階的辦法(useReducer)☆☆☆

回頭看第一個例子,為什麼在useEffect中不新增count就無法實現想要的定時器效果呢,說白了是因為讀取了state的資料,而又因為閉包原因拿不到最新的count資料,所以導致interval操作失敗。其實藉助useReducer就可以在不讀取count的情況更新count資料。

import React, { useEffect, useReducer } from "react";

function reducer(state: { count: number }) {
  return { count: state.count + 1 };
}
export default function IntervalExp() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  useEffect(() => {
    setInterval(() => {
      dispatch();
    }, 1000);
  }, []);

  return (
    <div>
      <p>當前計數{state.count}</p>
    </div>
  );
}

在這個案例中,使用useReducer定義了一個簡單的count操作方法,在interval中,通過呼叫dispatch方法,成功更新了count資料。useReducer在需要操作多個state的複雜業務邏輯場景下可以使用,雖然定義起來麻煩,但是可以實現將元件中的業務邏輯抽離出來,寫出更加易於維護的程式碼,而且在目前這個場景中,useReducer比上面兩個方式處理的更加優雅,也是本文推薦的方式。

總結

hook是React中非常有魅力的一個發明,靈活使用hook可以寫出更有品質的程式碼。作者寫本文的目的也是因為在實際開發中遇到了這一問題,因此希望本文可以幫助到其他開發者。

參考文章:usestate中的回撥函式_React Hooks 中使用 setInterval 的若干方法

相關文章