【React Hooks 專題】useEffect 使用指南
引言
Hooks 是 React 16.8 的新增特性,至今經歷兩年的時間,它可以讓你在不編寫 class 組件的情況下使用 state 以及其他 React 特性。useEffect
是基礎 Hooks 之一,我在項目中使用較爲頻繁,但總有些疑惑 ,比如:
-
如何正確使用
useEffect
? -
useEffect
的執行時機 ? -
useEffect
和生命週期的區別 ?
本文主要從以上幾個方面分析 useEffect
,以及與另外一個看起來和 useEffect
很像的 Hook useLayoutEffect
的使用和它們之間的區別。
useEffect 簡介
首先介紹兩個概念,純函數和副作用函數。純函數( Pure Function ):對於相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,這樣的函數被稱爲純函數。副作用函數( Side effect Function ):如果一個函數在運行的過程中,除了返回函數值,還對主調用函數產生附加的影響,這樣的函數被稱爲副作用函數。useEffect
就是在 React 更新 DOM 之後運行一些額外的代碼,也就是執行副作用操作,比如請求數據,設置訂閱以及手動更改 React 組件中的 DOM 等。
正確使用 useEffect
基本使用方法:useEffect(effect)
根據傳參個數和傳參類型,useEffect(effect)
的執行次數和執行結果是不同的,下面一一介紹。
- 默認情況下,
effect
會在每次渲染之後執行。示例如下:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除訂閱
subscription.unsubscribe();
};
});
- 也可以通過設置第二個參數,依賴項組成的數組
useEffect(effect,[])
,讓它在數組中的值發生變化的時候執行,數組中可以設置多個依賴項,其中的任意一項發生變化,effect
都會重新執行。示例如下:
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。
- 當依賴項是一個空數組 [] 時 ,
effect
只在第一次渲染的時候執行。
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
函數不需要同步執行,不會像 componentDidMount
或 componentDidUpdate
那樣阻塞瀏覽器更新屏幕。
所以 useEffect
可以被看作是每一次渲染之後的一個獨立的函數 ,可以接收 props 和 state ,並且接收的 props 和 state 是當次 render 的數據,是獨立的 。相對於生命週期 componentDidMount
中的 this.state
始終指向最新數據, useEffect
中不一定是最新的數據,更像是渲染結果的一部分 —— 每個 useEffect
屬於一次特定的渲染。對比示例如下:
- 在 Function 組件中使用
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>
);
}
結果如下:
- 在 Class 組件中的使用生命週期,代碼示例:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
結果如下:
但是每次渲染之後都去執行 effect
並不高效。所以怎麼解決呢 ?這就需要我們告訴 React 對比依賴來決定是否執行 effect
。
如何準確綁定依賴
在 effect
中用到了哪些外部變量,都需要如實告訴 React ,那如果沒有正確設置依賴項會怎麼樣呢 ?示例如下 :
上面例子中, useEffect
中用到的依賴項 count
,卻沒有聲明在卸載依賴項數組中,useEffect
不會再重新運行(只打印了一次 useEffect ), effect
中 setInterVal
拿的 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