React.memo

TLNR

React.memo 通过对传入的组件打上 REACT_MEMO_TYPE 的标签后使得在每一次更新前对传入的 props 进行浅比较（或者通过 compare 函数对新旧 props 进行比较），来决定是否复用原来的 fiber 组件

如果当前组件内存在更新，那么 memo component 会跳过比较，直接生成新的 fiber

说明

分析基于以下代码

Copy
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 (

number is : {props.number}

);
}, isEqual);

function Child(props = {}) {
console.log(`--- re-render ---`);
return (

number is : {props.number}

);
};

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 (

step is : {step}
count is : {count}
numberis : {number}

<Child step={step} count={count} number={number} />
<ChildMemo step={step} count={count} number={number} />

);
}

以下是按左到右依次点击 button 后的输出，可以看到 memo 包裹的组件只有在满足 compare 函数后才会发生更新，而另外一个每次点击都会重新 render

从 React.memo 开始

对 ChildMemo 打上断点，我们会进入到 memo 内

Copy
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 阶段也是进这个函数创建节点的）

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