TLNR#
- React.memo 透過對傳入的組件打上 REACT_MEMO_TYPE 的標籤後使得在每一次更新前對傳入的 props 進行淺比較(或者透過 compare 函數對新舊 props 進行比較),來決定是否重用原來的 fiber 組件
- 如果當前組件內存在更新,那麼 memo component 會跳過比較,直接生成新的 fiber
說明#
分析基於以下代碼
import React, { useState, memo } from 'react';
const isEqual = (prevProps, nextProps) => {
if (prevProps.number !== nextProps.number) {
return false;
}
return true;
}
const ChildMemo = memo((props = {}) => {
console.log(`--- memo re-render ---`);
return (
<div>
<p>number is : {props.number}</p>
</div>
);
}, isEqual);
function Child(props = {}) {
console.log(`--- re-render ---`);
return (
<div>
<p>number is : {props.number}</p>
</div>
);
};
export default function ReactMemo(props = {}) {
const [step, setStep] = useState(0);
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
const handleSetStep = () => {
setStep(step + 1);
}
const handleSetCount = () => {
setCount(count + 1);
}
const handleCalNumber = () => {
setNumber(count + step);
}
return (
<div>
<button onClick={handleSetStep}>step is : {step} </button>
<button onClick={handleSetCount}>count is : {count} </button>
<button onClick={handleCalNumber}>numberis : {number} </button>
<hr />
<Child step={step} count={count} number={number} /> <hr />
<ChildMemo step={step} count={count} number={number} />
</div>
);
}
以下是按左到右依次點擊 button 後的輸出,可以看到 memo 包裹的組件只有在滿足 compare 函數後才會發生更新,而另外一個每次點擊都會重新 render
從 React.memo 開始#
對 ChildMemo 打上斷點,我們會進入到 memo 內
export function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
... 刪除了 dev 邏輯
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
... 刪除了 dev 邏輯
return elementType;
}
邏輯很簡單,對包裹的組件打上 REACT_MEMO_TYPE 這一標籤,而這一步將會在構建 WIP 樹時(beginWork)產生影響 —— 也就是會進入到針對 MemoComponent 的邏輯內。
beginWork 如何處理 MemoComponent#
直接對 beginWork 內的 case MemoComponent 打上斷點,隨意觸發一個更新,便會進入對 memo 的邏輯(別看名字叫 updateMemoComponent,mount 階段也是進這個函數創建節點的)
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
// current 為空,對應的是 mount 階段的事
// 當 current 為空時則創建對應的 fiber 節點(createFiberFromTypeAndProps),畢竟沒有比較的對象直接創建就行
if (current === null) {
const type = Component.type;
...
... 刪除了 dev 邏輯
const child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
workInProgress,
workInProgress.mode,
renderLanes,
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
... 刪除了 dev 邏輯
// 這裡因為存在 current 節點則進入更新邏輯
const currentChild = ((current.child: any): Fiber); // This is always exactly one child
// 這裡需要提一嘴,renderLanes 是當前的渲染優先級,也就是只有優先級與 renderLanes 相同的 update 才會在此次渲染中被處理
// 而每當一個 update 產生時便會將當前 update 的 lanes 標記到 root.pendingLanes 上,而 root.pendingLanes 上的最高優先級則會成為 renderLanes
// 這裡可以看一下 React Hooks: useState,我在那篇文章內有更詳細的介紹
// 底下這個函數其實就是在當前的 fiber 節點上檢查是否存在與 renderLanes 相同優先級的 update 實例,如果存在那麼則意味著這個更新
// 必須在本輪 render 內處理掉,那麼也就意味著即使傳入的 props 沒有發生變化(或者說 compare 函數返回 true),這個組件依然會被更新
// 那麼我們就得到了 TLNR 中對應的第二條的結論
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
// 如果當前 fiber 上不存在需要立即被更新的 update,
// 那麼進入 if 內的邏輯
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
// 如果 compare 不存在,也就是 React.memo 的第二個參數沒有傳入的話就會使用默認的 shallowEqual
// shallowEqual 是一個淺比較,只比較兩個 props 的引用地址是否發生變化
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
// 如果 compare 返回了 true,那麼意味著 memo 包裹的節點可以被重用,於是調用 bailoutOnAlreadyFinishedWork 重用 current 樹上的節點
// 這樣就不需要重複生成節點,從而優化了性能
// bailoutOnAlreadyFinishedWork 這個函數的解析我後續補充到附錄內
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
// 運行到這裡則說明當前 fiber 上存在需要立即被更新的 update
// 那就只能重新生成節點了
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}