【React Hooks 專題】useEffect 使用指南

引言

Hooks 是 React 16.8 的新增特性,至今經歷兩年的時間,它可以讓你在不編寫 class 組件的情況下使用 state 以及其他 React 特性。useEffect 是基礎 Hooks 之一,我在項目中使用較爲頻繁,但總有些疑惑 ,比如:

本文主要從以上幾個方面分析 useEffect ,以及與另外一個看起來和 useEffect 很像的 Hook useLayoutEffect 的使用和它們之間的區別。

useEffect 簡介

首先介紹兩個概念,純函數和副作用函數。純函數( Pure Function ):對於相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,這樣的函數被稱爲純函數。副作用函數( Side effect Function ):如果一個函數在運行的過程中,除了返回函數值,還對主調用函數產生附加的影響,這樣的函數被稱爲副作用函數。useEffect 就是在 React 更新 DOM 之後運行一些額外的代碼,也就是執行副作用操作,比如請求數據,設置訂閱以及手動更改 React 組件中的 DOM 等。

正確使用 useEffect

基本使用方法:useEffect(effect)根據傳參個數和傳參類型,useEffect(effect) 的執行次數和執行結果是不同的,下面一一介紹。

useEffect(() ={
  const subscription = props.source.subscribe();
  return () ={
    // 清除訂閱
    subscription.unsubscribe();
  };
});
useEffect(
  () ={
    const subscription = props.source.subscribe();
    return () ={
      subscription.unsubscribe();
    };
  },
  [props.source],
);

需要注意的是:當依賴項是引用類型時,React 會對比當前渲染下的依賴項和上次渲染下的依賴項的內存地址是否一致,如果一致,effect 不會執行,只有當對比結果不一致時,effect 纔會執行。示例如下:

function Child(props) {
  
  useEffect(() ={
    console.log("useEffect");
  }[props.data]);
  
  return <div>{props.data.x}</div>;
}

let b = { x: 1 };

function Parent() {
  const [count, setCount] = useState(0);
  console.log("render");
  return (
    <div>
      <button
        onClick={() ={
          b.x = b.x + 1;
          setCount(count + 1);
        }}
      >
        Click me
      </button>
      <Child data={b} />
    </div>
  );
}

結果如下:

上面實例中,組件 <Child/> 中的 useEffect 函數中的依賴項是一個對象,當點擊按鈕對象中的值發生變化,但是傳入 <Child/>  組件的內存地址沒有變化,所以 console.log("useEffect") 不會執行,useEffect 不會被打印。爲了解決這個問題,我們可以使用對象中的屬性作爲依賴,而不是整個對象。把上面示例中組件 <Child/> 修改如下:

function Child(props) {
  
  useEffect(() ={
    console.log("useEffect");
  }[props.data.x]);
  
  return <div>{props.data.x}</div>;
}

修改後結果如下:

可見 useEffect 函數中的 console.log("useEffect") 被執行,打印出 useEffect。

useEffect 的執行時機

默認情況下,effect 在第一次渲染之後和每次更新之後都會執行,也可以是隻有某些值發生變化之後執行,重點在於是每輪渲染結束後延遲調用( 異步執行 ),這是 useEffect 的好處,保證執行 effect 的時候,DOM 都已經更新完畢,不會阻礙 DOM 渲染,造成視覺阻塞。

useEffect 和 useLayoutEffect 的區別

useLayoutEffect 的使用方法和 useEffect 相同,區別是他們的執行時機。

如上面所說,effect 的內容是會在渲染 DOM 之後執行,然而並非所有的操作都能被放在 effect 都延遲執行的,例如,在瀏覽器執行下一次繪製前,需要操作 DOM 改變頁面樣式,如果放在 useEffect 中執行,會出現閃屏問題。而 useLayoutEffect 是在瀏覽器執行繪製之前被同步執行,放在 useLayoutEffect 中就會避免這個問題。

這篇文章中可以清楚的看到上述例子的具體實現:useEffect 和 useLayoutEffect 的區別

對比 useEffect 和生命週期

如果你熟悉生命週期函數,你可能會用生命週期的思路去類比思考 useEffect 的執行過程,但其實並不建議這麼做,因爲 useEffect 的心智模型和 componentDidMount 等其他生命週期是不同的。

Function 組件中不存在生命週期,React 會根據我們當前的 props 和 state 同步 DOM ,每次渲染都會被固化,包括 state、props、side effects 以及寫在 Function 組件中的所有函數。

另外,大多數 useEffect 函數不需要同步執行,不會像 componentDidMountcomponentDidUpdate 那樣阻塞瀏覽器更新屏幕。

所以 useEffect 可以被看作是每一次渲染之後的一個獨立的函數 ,可以接收 props 和 state ,並且接收的 props 和 state 是當次 render 的數據,是獨立的 。相對於生命週期 componentDidMount 中的 this.state 始終指向最新數據, useEffect 中不一定是最新的數據,更像是渲染結果的一部分 —— 每個 useEffect 屬於一次特定的渲染。對比示例如下:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() ={
    setTimeout(() ={
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

結果如下:

  componentDidUpdate() {
    setTimeout(() ={
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }

結果如下:

但是每次渲染之後都去執行 effect 並不高效。所以怎麼解決呢 ?這就需要我們告訴 React 對比依賴來決定是否執行 effect

如何準確綁定依賴

effect 中用到了哪些外部變量,都需要如實告訴 React ,那如果沒有正確設置依賴項會怎麼樣呢 ?示例如下 :

上面例子中, useEffect 中用到的依賴項 count,卻沒有聲明在卸載依賴項數組中,useEffect 不會再重新運行(只打印了一次 useEffect ), effectsetInterVal 拿的 count 始終是初始化的 0 ,它後面每一秒都會調用 setCount(0 + 1) ,得到的結果始終是 1 。下面有兩種可以正確解決依賴的方法:

1. 在依賴項數組中包含所有在 effect 中用到的值

effect 中用到的外部變量 count 如實添加到依賴項數組中,結果如下:

可以看到依賴項數組是正確的,並且解決了上面的問題,但是也可以發現,隨之帶來的問題是:定時器會在每一次 count 改變後清除和重新設定,重複創建 / 銷燬,這不是我們想要的結果。

2. 第二種方法是修改 effect 中的代碼來減少依賴項

即修改 effect 內部的代碼讓 useEffect 使得依賴更少,需要一些移除依賴常用的技巧,如:setCount 還有一種函數回調模式,你不需要關心當前值是什麼,只要對 “舊的值” 進行修改即可,這樣就不需要通過把 count 寫到依賴項數組這種方式來告訴 React 了,因爲 React 已經知道了。

是否需要清除副作用

若只是在 React 更新 DOM 之後運行一些額外的代碼,比如發送網絡請求,手動變更 DOM,記錄日誌,無需清除操作,因爲執行之後就可以被忽略。

需要清除的是指那些執行之後還有後續的操作,比如說監聽鼠標的點擊事件,爲防止內存泄漏清除函數將在組件卸載之前調用,可以通過 useEffect 的返回值銷燬通過 useEffect 註冊的監聽。

清除函數執行時機是在新的渲染之後進行的,示例如下(點擊在線測試):

const Example = () ={
  const [count, setCount] = useState(0);

  useEffect(() ={
    console.log("useEffect");
    return () ={
      console.log("return");
    };
  }[count]);

  return (
    <div>
      <p>You Click {count} times </p>
      {console.log("dom")}
      <button
        onClick={() ={
          setCount(count + 1);
        }}
      >
        Click me
      </button>
    </div>
  );
};

結果如下:

需要注意的是useEffect 的清除函數在每次重新渲染時都會執行,而不是隻在卸載組件的時候執行 。

參考文檔

React Core Team 成員、Readux 作者 Dan 對 useEffect 的完全解讀  ---  A Complete Guide to useEffect

關於作者

Starry , Web 前端工程師,就職於民生銀行後端平臺研發團隊,螢火蟲實驗室成員,目前負責仿真服務平臺前端開發工作。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pcycdRnrP93dLs7I60tWIQ