从源码角度看React中的memo

556 阅读5分钟

最近决定看一下react源码中关于memo,useMemo,useCallback,目前看到了memo部分就准备写一篇文章,后续应该会继续更新。

先来看memeo的源码吧:

memo源码

以下是忽略 __DEV__ 后的简化版 memo

export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
  return elementType;
}

其中:

  • type:一个有效的React组件
  • compare:可选的自定义比较函数,用于判断新旧 props 是否相等。如果未提供,则使用默认的浅比较

可以看出,memo源码中,创建了一个elementType对象,其中

  • 设置 $$typeof: REACT_MEMO_TYPE,可以使得react将其与普通对象相区别,从而快速判断这个组件是一个memo包装的组件
  • type:原始的组件(即传入的 type 参数)。
  • compare:自定义的比较函数(如果未提供,则为 null)。

memo的作用

  • 避免不必要的重新渲染:当组件的 props 没有发生变化时,跳过组件的重新渲染
  • 通过比较新旧 props 来决定是否需要更新:默认情况下,React.memo 使用浅比较来判断 props 是否发生变化。开发者也可以提供自定义的比较函数。

浅比较逻辑

那么问题来了,memo源码中并没有浅比较的代码,他是怎么实现浅比较的呢?

因为浅比较的逻辑是由 React 的渲染器(Reconciler)在组件更新时处理的,为不是在memo中实现

这种设计的好处是分离关注点,使代码更加模块化和可维护。

而浅比较的代码是下面这样的:

/**
   * Performs equality by iterating through keys on an object and returning false
   * when any key has values which are not strictly equal between the arguments.
   * Returns true when the values of all keys are strictly equal.
   */
  //执行通过遍历对象的键并返回false,当任何键的值在参数之间不是严格相等时。
  //当所有键的值都严格相等时返回true。
  function shallowEqual(objA: mixed, objB: mixed): boolean {
    //如果两个值完全相等(包括 NaN 和 -0 的特殊情况),直接返回 true
    if (Object.is(objA, objB)) {//基础数据类型直接比较值,引用数据类型比较引用地址
      return true;
    }

    //如果任意一个不是对象,或者为 null,则返回 false
    if (
      typeof objA !== 'object' ||
      objA === null ||
      typeof objB !== 'object' ||
      objB === null
    ) {
      return false;
    }

    //获取两个对象的键,并比较键的数量
    const keysA = Object.keys(objA);
    const keysB = Object.keys(objB);
    if (keysA.length !== keysB.length) {
      return false;
    }

    //遍历键,检查每个键是否存在于另一个对象,并且对应的值是否相等
    for (let i = 0; i < keysA.length; i++) {
      if (
        !hasOwnProperty.call(objB, keysA[i]) || // 检查键是否存在
        !Object.is(objA[keysA[i]], objB[keysA[i]]) // 比较值是否相等
      ) {
        return false;
      }
    }

    // 如果所有检查都通过,则返回 true
    return true;
  }
逐步解析
  1. 使用 Object.is 判断是否完全相等

    • 如果两个值完全相等(包括 NaN-0 的特殊情况),直接返回 true
    • 这一步可以快速排除大部分简单的情况。
  2. 检查类型和 null

    • 如果任意一个值不是对象(例如是原始值),或者为 null,则直接返回 false
    • 因为只有对象才需要进一步比较键和值。
  3. 比较键的数量

    • 如果两个对象的键数量不同,则它们不可能相等,直接返回 false
  4. 遍历键并比较值

    • 遍历对象 objA 的所有键,检查:

      • 该键是否也存在于 objB 中(通过 hasOwnProperty 检查)。
      • 两个对象在该键上的值是否相等(通过 Object.is 比较)。
    • 如果有任何一个键不满足条件,则返回 false

  5. 返回结果

    • 如果所有键和值都匹配,则返回 true

至此就完成了浅比较

那么有了一个疑问,浅比较函数是在哪调用的呢?

再看源码

packages/react-reconciler/src/ReactFiberBeginWork.js文件中有一个updateMemoComponent方法,他是实现memeo的关键,然后以下是memo的主要部分

const currentChild = ((current.child: any): Fiber); // This is always exactly one child

  //判断当前 Fiber 节点是否存在需要处理的更新(Update)或上下文(Context)变更
  //都完成了返回true,否则返回false
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
    current,
    renderLanes,
  );
  if (!hasScheduledUpdateOrContext) {
    // This will be the props with resolved defaultProps,
    // unlike current.memoizedProps which will be the unresolved ones.
    // nextProps 将会是已经应用了 defaultProps 的 props,
    // 而 current.memoizedProps 保存的是未应用 defaultProps 的原始 props。

    // 获取已解析的旧 props(包含 defaultProps 处理后的结果)
    const prevProps = currentChild.memoizedProps;
    // Default to shallow comparison

    // 使用自定义比较函数或默认的浅比较
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    // 比较 props 和 ref
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
       // 跳过子组件更新
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // 创建新的子 Fiber
  const newChild = createWorkInProgress(currentChild, nextProps);
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}

updateMemoComponent函数首先判断当前的fiber节点是否存在需要处理的更新(组件自身调用setState等,不包括父组件传递props变化)或者上下文变更(仅检测通过 useContext 订阅的 Context 值的变化)

如果都完成了(无需更改),就进入if,看有没有自定义比较函数,没有就使用浅比较,比较新旧组件compare(prevProps, nextProps),是同一个就调用 bailoutOnAlreadyFinishedWork 函数来阻止组件重新渲染跳过子组件更新(跳过渲染)

如果没有完成(需要更改)就创建新的fiber节点

bailoutOnAlreadyFinishedWork 的核心逻辑如下:

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    //如果当前组件可以跳过重新渲染,则直接复用之前的依赖(如上下文依赖)。
    workInProgress.dependencies = current.dependencies;
  }
  //标记该组件跳过的更新优先级(lanes)
  markSkippedUpdateLanes(workInProgress.lanes);
  //检查子节点是否也有未完成的工作。
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 子节点没有需要处理的更新时直接返回 null
    if (current !== null) {
        //当父组件跳过更新时,检查并传播可能遗漏的上下文变化到子组件
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
    }
  }
  //当子树需要更新时,克隆子 Fiber 节点。
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}