說明#
- useCallback 與 useMemo 作為優化性的 hooks,可以幫助減少 react 組件內不必要的渲染與計算,但本文只分析這兩個 hooks 的實現,不講解如何使用,版本基於 v18.1.0
- 閱讀本文需先閱讀 React Hooks: hooks 鏈表
- 和其他大多數 hooks 一樣,每個 hooks 都按照 mount 與 update 兩個階段講解
- 分析基於以下代碼
import { useCallback, useEffect, useMemo, useState } from "react";
function UseCallbackAndUseMemo() {
const [a, setA] = useState('a');
const [b] = useState('b');
const memoizedCallback = useCallback(
() => {
console.log(a, b);
},
[a, b],
);
const memoizedResult = useMemo(() => {
return {
a,
b
}
}, [a, b]);
useEffect(() => {
setTimeout(() => {
setA('aaa');
}, 3000);
}, []);
useEffect(() => {
console.log(memoizedResult, 'memoizedResult change');
}, [
memoizedResult
]);
return (
<div onClick={ memoizedCallback }>click here</div>
)
}
export default UseCallbackAndUseMemo;
useCallback#
mount 階段的 useCallback#
對 useCallback 打上斷點,會進入 mountCallback
來看看裡面是什麼東西
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 生成 hook 對象的,見 hook 鏈表內的分析
const hook = mountWorkInProgressHook();
// 赋值
const nextDeps = deps === undefined ? null : deps;
// 把函數和依賴存到 hook 對象的 memoizedState 上
hook.memoizedState = [callback, nextDeps];
// 返回函數
return callback;
}
沒了。。。那就再看看 update 幹了啥
update 階段的 useCallback#
保持斷點不變,等定時器執行,隨後進入 updateCallback
來看看裡面是什麼東西
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 移動 hooks 鏈表的指針,讓指針指向當前 useCallback 對應的 hook 對象上
const hook = updateWorkInProgressHook();
// 獲取 update 階段傳入的依賴
const nextDeps = deps === undefined ? null : deps;
// 獲取上一次存在 hook 對象上的 [callback, deps]
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
/**
* prevState 不為空時(不過也不可能為空啊。。)
* nextDeps 也不為空時進入
*/
// 獲取 memoizedState 內的 依賴項(deps)
const prevDeps: Array<mixed> | null = prevState[1];
// 比較前後依賴是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 相同就把上一次緩存的函數 return 出去
return prevState[0];
}
}
}
// 如果上述條件不滿足,就更新 memoizedState 內的數據
hook.memoizedState = [callback, nextDeps];
// 返回函數
return callback;
}
大致上就是通過比較前後依賴項來決定是否復用上一次緩存的函數,一個類似閉包的實現。舉例來解釋的話,類似下面的代碼。
function exampleA(a) {
let number = a;
return function example() {
return a + 2
}
}
function exampleB(b) {
let number = b;
return function example() {
return b + 2
}
}
const func1 = exampleA(2);
const func2 = exampleB(2);
當兩個函數都傳入同一個值時,雖然都會得到執行結果都為 4 的函數,但 func1 與 func2 絕不是同一個函數。如果傳入到 react 的組件內必然會導致 props 發生變化從而導致重新渲染組件,但即使重新渲染後,頁面也不會發生變化。所以為了防止出現沒有必要的重渲染,索性就用了執行結果相同的緩存函數。
接下來看看 areHookInputsEqual 是怎麼比較前後依賴的
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
// 同時遍歷兩個數組,然後一一比較
// is 是個淺比較 ===
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
useCallback 總結#
useCallback 通過將函數與依賴存入到對應的 hook 對象上,在更新時會通過對前後依賴進行一一的淺比較來判斷函數是否可以復用,從而來實現復用函數,避免組件發生不必要的更新。
useMemo#
mount 階段的 useMemo#
對 useMemo 打點,會進入 mountMemo
/**
* 和 useCallBack 的 mount 階段幾乎一樣的邏輯
*/
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 唯一的不同在這裡,useMemo 存的是函數執行後的結果
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
update 階段的 useMemo#
保持斷點不變,等定時器執行,隨後進入 updateMemo
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// 假設這些是定義的。如果它們不是,areHookInputsEqual 會警告。
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
還是和 useCallback 的 update 階段一模一樣的邏輯。不講了。
useMemo 總結#
useMemo 與 useCallback 的實現幾乎一致,唯一的不同是,useMemo 存儲函數運行後的結果,useCallback 則是存儲函數本身。