【React Hooks 專題】useEffect 使用指南
引言
Hooks 是 React 16.8 的新增特性,至今經(jīng)歷兩年的時(shí)間,它可以讓你在不編寫(xiě) class 組件的情況下使用 state 以及其他 React 特性。useEffect 是基礎(chǔ) Hooks 之一,我在項(xiàng)目中使用較為頻繁,但總有些疑惑 ,比如:
如何正確使用 useEffect?useEffect的執(zhí)行時(shí)機(jī) ?useEffect和生命周期的區(qū)別 ?
本文主要從以上幾個(gè)方面分析 useEffect ,以及與另外一個(gè)看起來(lái)和 useEffect 很像的 Hook useLayoutEffect 的使用和它們之間的區(qū)別。
useEffect 簡(jiǎn)介
首先介紹兩個(gè)概念,純函數(shù)和副作用函數(shù)。純函數(shù)( Pure Function ):對(duì)于相同的輸入,永遠(yuǎn)會(huì)得到相同的輸出,而且沒(méi)有任何可觀察的副作用,這樣的函數(shù)被稱為純函數(shù)。副作用函數(shù)( Side effect Function ):如果一個(gè)函數(shù)在運(yùn)行的過(guò)程中,除了返回函數(shù)值,還對(duì)主調(diào)用函數(shù)產(chǎn)生附加的影響,這樣的函數(shù)被稱為副作用函數(shù)。useEffect 就是在 React 更新 DOM 之后運(yùn)行一些額外的代碼,也就是執(zhí)行副作用操作,比如請(qǐng)求數(shù)據(jù),設(shè)置訂閱以及手動(dòng)更改 React 組件中的 DOM 等。
正確使用 useEffect
基本使用方法:useEffect(effect)根據(jù)傳參個(gè)數(shù)和傳參類型,useEffect(effect) 的執(zhí)行次數(shù)和執(zhí)行結(jié)果是不同的,下面一一介紹。
默認(rèn)情況下, effect會(huì)在每次渲染之后執(zhí)行。示例如下:
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// 清除訂閱
subscription.unsubscribe();
};
});
也可以通過(guò)設(shè)置第二個(gè)參數(shù),依賴項(xiàng)組成的數(shù)組 useEffect(effect,[]),讓它在數(shù)組中的值發(fā)生變化的時(shí)候執(zhí)行,數(shù)組中可以設(shè)置多個(gè)依賴項(xiàng),其中的任意一項(xiàng)發(fā)生變化,effect都會(huì)重新執(zhí)行。示例如下:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
需要注意的是:當(dāng)依賴項(xiàng)是引用類型時(shí),React 會(huì)對(duì)比當(dāng)前渲染下的依賴項(xiàng)和上次渲染下的依賴項(xiàng)的內(nèi)存地址是否一致,如果一致,effect 不會(huì)執(zhí)行,只有當(dāng)對(duì)比結(jié)果不一致時(shí),effect 才會(huì)執(zhí)行。示例如下:
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>
);
}
結(jié)果如下:

上面實(shí)例中,組件 <Child/> 中的 useEffect 函數(shù)中的依賴項(xiàng)是一個(gè)對(duì)象,當(dāng)點(diǎn)擊按鈕對(duì)象中的值發(fā)生變化,但是傳入 <Child/> 組件的內(nèi)存地址沒(méi)有變化,所以 console.log("useEffect") 不會(huì)執(zhí)行,useEffect 不會(huì)被打印。為了解決這個(gè)問(wèn)題,我們可以使用對(duì)象中的屬性作為依賴,而不是整個(gè)對(duì)象。把上面示例中組件 <Child/> 修改如下:
function Child(props) {
useEffect(() => {
console.log("useEffect");
}, [props.data.x]);
return <div>{props.data.x}</div>;
}
修改后結(jié)果如下:

可見(jiàn) useEffect 函數(shù)中的 console.log("useEffect") 被執(zhí)行,打印出 useEffect。
當(dāng)依賴項(xiàng)是一個(gè)空數(shù)組 [] 時(shí) , effect只在第一次渲染的時(shí)候執(zhí)行。
useEffect 的執(zhí)行時(shí)機(jī)
默認(rèn)情況下,effect 在第一次渲染之后和每次更新之后都會(huì)執(zhí)行,也可以是只有某些值發(fā)生變化之后執(zhí)行,重點(diǎn)在于是每輪渲染結(jié)束后延遲調(diào)用( 異步執(zhí)行 ),這是 useEffect 的好處,保證執(zhí)行 effect 的時(shí)候,DOM 都已經(jīng)更新完畢,不會(huì)阻礙 DOM 渲染,造成視覺(jué)阻塞。
useEffect 和 useLayoutEffect 的區(qū)別
useLayoutEffect 的使用方法和 useEffect 相同,區(qū)別是他們的執(zhí)行時(shí)機(jī)。
如上面所說(shuō),effect 的內(nèi)容是會(huì)在渲染 DOM 之后執(zhí)行,然而并非所有的操作都能被放在 effect 都延遲執(zhí)行的,例如,在瀏覽器執(zhí)行下一次繪制前,需要操作 DOM 改變頁(yè)面樣式,如果放在 useEffect 中執(zhí)行,會(huì)出現(xiàn)閃屏問(wèn)題。而 useLayoutEffect 是在瀏覽器執(zhí)行繪制之前被同步執(zhí)行,放在 useLayoutEffect 中就會(huì)避免這個(gè)問(wèn)題。
這篇文章中可以清楚的看到上述例子的具體實(shí)現(xiàn):useEffect 和 useLayoutEffect 的區(qū)別
對(duì)比 useEffect 和生命周期
如果你熟悉生命周期函數(shù),你可能會(huì)用生命周期的思路去類比思考 useEffect 的執(zhí)行過(guò)程,但其實(shí)并不建議這么做,因?yàn)?useEffect 的心智模型和 componentDidMount 等其他生命周期是不同的。
Function 組件中不存在生命周期,React 會(huì)根據(jù)我們當(dāng)前的 props 和 state 同步 DOM ,每次渲染都會(huì)被固化,包括 state、props、side effects 以及寫(xiě)在 Function 組件中的所有函數(shù)。
另外,大多數(shù) useEffect 函數(shù)不需要同步執(zhí)行,不會(huì)像 componentDidMount 或 componentDidUpdate 那樣阻塞瀏覽器更新屏幕。
所以 useEffect 可以被看作是每一次渲染之后的一個(gè)獨(dú)立的函數(shù) ,可以接收 props 和 state ,并且接收的 props 和 state 是當(dāng)次 render 的數(shù)據(jù),是獨(dú)立的 。相對(duì)于生命周期 componentDidMount 中的 this.state 始終指向最新數(shù)據(jù), useEffect 中不一定是最新的數(shù)據(jù),更像是渲染結(jié)果的一部分 —— 每個(gè) useEffect 屬于一次特定的渲染。對(duì)比示例如下:
在 Function 組件中使用 useEffect代碼示例 (點(diǎn)擊在線測(cè)試):
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>
);
}
結(jié)果如下:

在 Class 組件中的使用生命周期,代碼示例:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
結(jié)果如下:

但是每次渲染之后都去執(zhí)行 effect 并不高效。所以怎么解決呢 ?這就需要我們告訴 React 對(duì)比依賴來(lái)決定是否執(zhí)行 effect 。
如何準(zhǔn)確綁定依賴
在 effect 中用到了哪些外部變量,都需要如實(shí)告訴 React ,那如果沒(méi)有正確設(shè)置依賴項(xiàng)會(huì)怎么樣呢 ?示例如下 :

上面例子中, useEffect 中用到的依賴項(xiàng) count,卻沒(méi)有聲明在卸載依賴項(xiàng)數(shù)組中,useEffect 不會(huì)再重新運(yùn)行(只打印了一次 useEffect ), effect 中 setInterVal 拿的 count 始終是初始化的 0 ,它后面每一秒都會(huì)調(diào)用 setCount(0 + 1) ,得到的結(jié)果始終是 1 。下面有兩種可以正確解決依賴的方法:
1.在依賴項(xiàng)數(shù)組中包含所有在 effect 中用到的值
將 effect 中用到的外部變量 count 如實(shí)添加到依賴項(xiàng)數(shù)組中,結(jié)果如下:

可以看到依賴項(xiàng)數(shù)組是正確的,并且解決了上面的問(wèn)題,但是也可以發(fā)現(xiàn),隨之帶來(lái)的問(wèn)題是:定時(shí)器會(huì)在每一次 count 改變后清除和重新設(shè)定,重復(fù)創(chuàng)建/銷毀,這不是我們想要的結(jié)果。
2.第二種方法是修改 effect 中的代碼來(lái)減少依賴項(xiàng)
即修改 effect 內(nèi)部的代碼讓 useEffect 使得依賴更少,需要一些移除依賴常用的技巧,如:setCount 還有一種函數(shù)回調(diào)模式,你不需要關(guān)心當(dāng)前值是什么,只要對(duì) “舊的值” 進(jìn)行修改即可,這樣就不需要通過(guò)把 count 寫(xiě)到依賴項(xiàng)數(shù)組這種方式來(lái)告訴 React 了,因?yàn)?React 已經(jīng)知道了。

是否需要清除副作用
若只是在 React 更新 DOM 之后運(yùn)行一些額外的代碼,比如發(fā)送網(wǎng)絡(luò)請(qǐng)求,手動(dòng)變更 DOM,記錄日志,無(wú)需清除操作,因?yàn)閳?zhí)行之后就可以被忽略。
需要清除的是指那些執(zhí)行之后還有后續(xù)的操作,比如說(shuō)監(jiān)聽(tīng)鼠標(biāo)的點(diǎn)擊事件,為防止內(nèi)存泄漏清除函數(shù)將在組件卸載之前調(diào)用,可以通過(guò) useEffect 的返回值銷毀通過(guò) useEffect 注冊(cè)的監(jiān)聽(tīng)。
清除函數(shù)執(zhí)行時(shí)機(jī)是在新的渲染之后進(jìn)行的,示例如下(點(diǎn)擊在線測(cè)試):
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>
);
};
結(jié)果如下:

需要注意的是:useEffect 的清除函數(shù)在每次重新渲染時(shí)都會(huì)執(zhí)行,而不是只在卸載組件的時(shí)候執(zhí)行 。
參考文檔
React Core Team 成員、Readux 作者 Dan 對(duì) useEffect 的完全解讀 --- A Complete Guide to useEffect
關(guān)于作者
Starry , Web 前端工程師,就職于民生銀行后端平臺(tái)研發(fā)團(tuán)隊(duì),螢火蟲(chóng)實(shí)驗(yàn)室成員,目前負(fù)責(zé)仿真服務(wù)平臺(tái)前端開(kāi)發(fā)工作。

