Skip to content

React.memo #9

Description

@IWSR

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions