前言
React Hooks的基本用法,官方文件 已經非常詳細。本文的目的,是想通過一個簡單的例子詳細分析一些令人疑惑的問題及其背後的原因。這是系列的第二篇,主要講解 useEffect。
個人部落格地址 ?? fe-code
類生命週期
官方文件中說,可以將 useEffect 的回撥和清理副作用的機制,類比成 class 元件中的生命週期。不過,由於 class 元件和函式元件自身特性不同的原因,導致這種類比也容易使人迷惑。
如果你熟悉 React class 的生命週期函式,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函式的組合。 --使用 Effect Hook
不過有時也容易出問題,就像我們一開始的定時器例子一樣。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return <h1>{count}</h1>;
}
複製程式碼
我們的需求很明確,就是在 componentDidMount 的時候,設定一個定時器。並且保證不會每次更新(componentDidUpdate)都重新設定。所以我們把第二個引數設定成[]
,來達到一樣的效果。
當然這是有問題的,由於函式式元件執行方式的不同,我們在 useEffect 中拿到的 count 是閉包引用的,而每次更新又會是一個全新的執行上下文。這在上一篇文章中已經詳細分析過。但是在 class 元件中,生命週期中的引用是這樣的 this.state.count
,而且不同於函式式,這種方式每次拿到的 count 都是最新的。
React Hooks 也提供了一個類似作用的 hook 來幫我們儲存一些值 — useRef,它可以很方便地儲存任何可變值,其類似於在 class 中使用例項欄位的方式
。不過這裡不太適用。
總的來說,useEffect 和真正的生命週期還是有些區別的,在使用的時候需要多加註意。
依賴
通過第一篇文章,我們已經瞭解了一些很重要的資訊,比如:每次更新都是一次重新執行。這不僅僅是對於 useState 來說的,整個函式元件都是這樣。不太瞭解的同學,可以先閱讀一下 深入 React hooks — useState。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return <h1>{count}</h1>;
}
複製程式碼
我們知道 每次更新都是一次重新執行。我們給 useEffect 的第二個引數傳的是 []
,所以可以達到回撥只執行一次的效果(只設定一次定時器)。
但是我們更應該知道的是,回撥函式只執行一次,並不代表 useEffect 只執行一次。在每次更新中,useEffect 依然會每次都執行,只不過因為傳遞給它的陣列依賴項是空的,導致 React 每次檢查的時候,都沒有發現依賴的變化,所以不會重新執行回撥。
檢查依賴,只是簡單的比較了一下值或者引用是否相等。
而且上面的寫法,官方是不推薦的。我們應該確保 useEffect 中用到的狀態(如:count ),都完整的新增到依賴陣列中。 不管引用的是基礎型別值、還是物件甚至是函式。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
// 不想用到外部狀態可以用 setCount(count => count + 1);
}, 1000);
}, [count]); // 確保所有狀態依賴都放在這裡
console.log(count);
return <h1>{count}</h1>;
}
複製程式碼
這樣才能保證回撥中可以每次拿到當前的 count 值。
副作用
咦!好像有什麼奇怪的東西。
發生了什麼不得了的事???
現在想想我們都幹了什麼。
- useEffect 回撥裡放了個定時器。
- 依賴陣列按要求寫了 count。
- 每次 count 改變引起的更新也會同時執行 useEffect 的回撥。
- 回撥裡的定時器也會重新設定。
- 嗯,好像發現問題了。
每次更新時,會重新執行 useEffect 的回撥函式,也就會重新設定一個定時器。但是有一個問題是,我們上一次設定的定時器並沒有清理掉,所以頻繁的更新會導致越來越多的定時器同時在執行。 為了解決上面的問題,就需要用到 useEffect 的另一個特性:清除副作用。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
// 返回一個清理副作用的函式
return () => {
clearInterval(id);
}
}, [count]);
console.log(count);
return <h1>{count}</h1>;
}
複製程式碼
ok,世界安靜了。
那麼,再思考個問題吧。useEffect 清理副作用的時機是什麼時候?在下一次檢視更新之前嗎?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(1, '我是定時器', count);
setCount(count + 1);
}, 1000);
return () => {
console.log(2, `我清理的是 ${count} 的副作用`);
clearInterval(id);
}
}, [count]);
console.log(3, '我是渲染', count);
return <h1>{count}</h1>;
}
複製程式碼
上面程式碼的列印順序會是 1、2、3 嗎?
顯然不是,useEffect 在檢視更新之後才清理上一次的副作用。這麼處理其實也是和 useEffect 的特性相契合的。React 只會在瀏覽器繪製後執行 useEffect。所以 Effect 的清除同樣被延遲了。上一次的 Effect 會在重新渲染後被清除。
小結
使用 useEffect 時,需要注意狀態的引用,依賴的新增以及副作用的清除(沒有就不用了)。很多時候還需要藉助其他的 hook 才能完成這個工作,比如 useRef/useCallback等。
參考文章
交流群
微信群:掃碼回覆加群。
後記
如果你看到了這裡,且本文對你有一點幫助的話,希望你可以動動小手支援一下作者,感謝?。文中如有不對之處,也歡迎大家指出,共勉。好了,又耽誤大家的時間了,感謝閱讀,下次再見!
- 文章倉庫 ??fe-code
感興趣的同學可以關注下我的公眾號 前端發動機,好玩又有料。