TLNR#
- React.memo は、渡されたコンポーネントに REACT_MEMO_TYPE のタグを付けることで、各更新前に渡された props を浅く比較(または compare 関数を使用して新旧 props を比較)し、元の fiber コンポーネントを再利用するかどうかを決定します。
- 現在のコンポーネント内に更新がある場合、memo コンポーネントは比較をスキップし、新しい 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>
);
}
以下は左から右にボタンをクリックした後の出力です。memo でラップされたコンポーネントは compare 関数を満たす場合にのみ更新され、もう一方は毎回再レンダリングされることがわかります。
从 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 と呼ばれていても、マウント段階でもこの関数に入ってノードを作成します)。
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
// currentがnullの場合、マウント段階のことに対応します
// currentがnullの場合は、対応する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); // これは常に正確に1つの子です
// ここで言及する必要があるのは、renderLanesは現在のレンダリング優先度であり、優先度がrenderLanesと同じ更新のみが今回のレンダリングで処理されることです
// 更新が発生するたびに、現在の更新のレーンがroot.pendingLanesにマークされ、root.pendingLanesの最高優先度がrenderLanesになります
// ここでReact Hooks: useStateを確認できます。その記事では詳細な説明があります
// 下の関数は、現在のfiberノード上でrenderLanesと同じ優先度のupdateインスタンスが存在するかどうかをチェックします。存在する場合、その更新は今回のレンダリング内で処理される必要があります。つまり、渡されたpropsが変更されていなくても(またはcompare関数がtrueを返しても)、このコンポーネントは依然として更新されます
// これにより、TLNRの対応する2つ目の結論が得られます
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
// 現在のfiber上に即座に更新される必要があるupdateが存在しない場合、
// その場合はif内のロジックに入ります
if (!hasScheduledUpdateOrContext) {
// これは解決されたdefaultPropsを持つpropsになります、
// current.memoizedPropsとは異なり、未解決のものです。
const prevProps = currentChild.memoizedProps;
// デフォルトで浅い比較を行います
let compare = Component.compare;
// compareが存在しない場合、つまりReact.memoの第2引数が渡されていない場合は、デフォルトのshallowEqualが使用されます
// shallowEqualは浅い比較で、2つの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はこのフラグを読み取ります。
workInProgress.flags |= PerformedWork;
// ここに到達した場合、現在のfiber上に即座に更新される必要があるupdateが存在します
// その場合はノードを再生成するしかありません
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}