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;
}