引言
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
将
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