IWSR / react-code-debug

react v18 源码分析
7 stars 1 forks source link

React.memo #9

Open IWSR opened 2 years ago

IWSR commented 2 years ago

React.memo

TLNR

  1. React.memo 通过对传入的组件打上 REACT_MEMO_TYPE 的标签后使得在每一次更新前对传入的 props 进行浅比较(或者通过 compare 函数对新旧 props 进行比较),来决定是否复用原来的 fiber 组件
  2. 如果当前组件内存在更新,那么 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

Nov-24-2022 00-07-23

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

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