從源碼理清 UseEffect 第二個參數是怎么處理的
useEffect 是常用的 hook,它支持兩個參數,第一個參數是回調函數,第二個參數是依賴。
當第二個參數為 null 或 undefined 的時候,回調函數每次 render 都會執行,而參數為數組的時候,只有依賴項變了才會執行。
這些我們都很熟悉了,但它是怎么實現的呢?我們來從源碼來找下答案。
useEffect 的第二個參數
我們先來試一下第二個參數傳入 undefined、空數組、有依賴的數組時的效果。
準備這樣一段代碼:
import { useEffect, useRef, useState } from 'react';
function Dong() {
const ref = useRef(1);
const [,setState] = useState();
useEffect(() => {
console.log(111);
});
useEffect(() => {
console.log(222);
}, []);
useEffect(() => {
console.log(333);
}, [ref.current]);
useEffect(() => {
setInterval(() => {
setState([]);
}, 1000);
setTimeout(() => {
ref.current = 2;
}, 3000);
}, []);
return <div>dong</div>;
}
我們用寫了三個 useEffect,第二個參數分別為 undefined、[]、有一個依賴的數組,回調函數里分別打印 111、222、333。
然后 useState 聲明了一個 state,用 setInterval 定時修改,這樣能不斷觸發 render。
又用 useRef 聲明了一個對象,它的特點是每次 render 都是返回的同一個對象,我們用 setTimeout 在 2s 后修改了它的值。
執行的結果大家應該很容易想到:
111 每次都會打印,因為第二個參數為 undefined。
222 只打印一次,因為第二個參數為 []。
333 打印兩次,因為第二個參數有一個依賴,這個依賴在 2s 的時候會變一次。
這些我們都很熟悉了,但是它為什么是這樣呢?
我們來看下源碼:
useEffect 相關源碼
react hooks 的原理前面一篇文章寫過,我們再過一遍:
jsx 編譯產生 render function,執行返回 vdom,但是為了提高性能,React 16 引入 fiber 架構,會先把 vdom 轉成 fiber,然后再去更新到 dom。
vdom 轉 fiber 的過程叫做 reconcile,更新到 dom 的過程叫做 commit。reconcile 的過程是可打斷的,需要 schedule。
hooks 也是基于 fiber 來實現的,它在 fiber 節點上維護了一個鏈表(memorizedState 屬性),用來保存數據,每個 hook 都是從對應的鏈表元素上存取各自的數據。
比如上面那個組件的 6 個 hook 就對應著 fiber 節點上 memorizedState 鏈表的 6 個元素:
每個 hook 都是在對應的鏈表元素上存取數據的。
這個鏈表有個建立的過程,叫做 mount,后面只需要 update,所以每個 hook 的實現都會分為 mount 和 update 兩個階段。
我們看下 useEffect 相關的源碼:
它也是分為了 mountEffect 和 updateEffect 兩個函數,最終都是在 hook.memorizedState 存取元素的。這就是 hook 的通用原理。
第二個參數對應的就是 deps,它是怎么判斷是否要更新的呢?
我們著重看下這段邏輯:
deps 是新傳入的參數,如果是 undefined 會作為 null。
hook.memorizedState.deps 取到的是之前的 deps。
然后新舊 deps 會做下對比,如果返回 true 才會執行 effect。
對比的邏輯在 areHookInputsEqual 這個函數里:
如果 prevDeps 是 null,那就直接返回 false,這就是 useEffect 第二個參數傳 undefined 或者 null 的話 effect 函數都會執行的原因。
否則,才會新舊的 deps 數組中每個元素做對比,有一個不一樣就返回 false。
這已經解釋了上面那個案例,deps 數組傳 undefined、[]、[dep] 時 effect 執行的不同情況。
其實還有一種情況也會導致 effect 執行,就是上面這段邏輯:
當熱更新的時候,就算依賴沒有變,也需要重新執行 effect,這個是通過 ignorePreviousDependencies 變量來控制的。
這個估計很多人都不知道,因為熱更新是工具實現的。
我們從源碼層面解釋清楚了 useEffect 第二個參數的處理機制。
其實 useCallback、useMemo 的 deps 參數處理邏輯也是一樣的,源碼都差不多:
總結
useEffect 第二個參數傳入 undefined、[]、[a,b,c] 時執行的效果不同, undefined 每次都會執行,而依賴數組只有在依賴變了才會執行,空數組只會執行一次。
我們從源碼層面解釋了原因:
hooks 是在 fiber 節點的 memorizedState 屬性上存取數據的,會組織一個和 hook 一一對應的鏈表。
構建這個鏈表的階段叫 mount,后面只需要 update,所以所有的 hook 的實現都分為了 mountXxx 和 updateXxx 兩部分。
useEffect 在 update 時會對比新傳入的 deps 和之前存在 memorizedState 上的 deps 來確定是否執行 effect 回調,它做了這樣的處理:
當 dep 是 null(undefined 也會處理成 null)時,判定為不相等。如果是熱更新的時候,判定為不相等。否則會對比數組的每個依賴項來判斷是否相等。只要新舊 deps 不相等就執行 effect。
useCallback、useMemo 的 deps 處理也是一樣的,我們從源碼層面理清楚了 deps 參數的處理機制。