实现一个 Mini React:核心功能详解 - 实现memo,从底层理解运行机制

205 阅读3分钟

xdm,又要到饭了,又更新代码了!

总结一下上一篇完成的内容,

  1. 完成了lazy / suspense的实现, 从底层理解运行机制。

有兴趣的可以点这里查看lazy / suspense的实现, 从底层理解运行机制

这一篇我们继续完成一个性能相关的api, memo。

memo 是优化 React 应用性能的有效手段之一。React.memo 是一个高阶组件(Higher-Order Component,HOC),用于在组件的 props 未发生变化时,跳过组件的重新渲染,从而提升性能。

理解 React.memo 的工作原理

React.memo 是一个高阶组件,用于优化函数组件。当传递给组件的 props 没有发生变化时,React.memo 会跳过组件的渲染过程,直接复用上一次渲染的结果。这对于避免不必要的渲染,提升应用性能尤其有效。

基本用法:

const MemoizedComponent = React.memo(Component, [areEqual]);

// 或者使用默认的浅比较
const MemoizedComponent = React.memo(Component);
  • Component:需要进行优化的函数组件。

  • areEqual(可选) :一个自定义的比较函数,用于判断 props 是否相等。返回 true 表示 props 相等,组件不需要重新渲染。

实现 memo 函数

实现步骤:

  1. 创建 memo 函数:接收一个组件和一个可选的比较函数,返回一个新的组件。
  2. 在 Fiber 节点中标记 memo:在 Fiber 树中标记被 memo 包装的组件,以便在调和阶段进行优化。
  3. 在调和阶段跳过不必要的渲染:当检测到 props 没有变化时,跳过该组件的渲染。

代码实现:

function memo(Component, areEqual) {
    return function MemoizedComponent(props) {
        return {
            type: Component,
            props,
            $$typeof: Symbol.for('react.memo')
            compare: areEqual || shallowCompare
        }
    }
}

function shallowCompare(prevProps, nextProps) {
    const prevKeys = Object.keys(prevProps)
    const nextKeys = Object.keys(nextProps)
    
    if (prevKeys.length !== nextKeys.length) {
        return false
    }
    
    
    for (let key of prevKeys) {
        if (prevKeys[key] !== nextProps[key]) {
            return false
        }
    }
    return true
}

集成 memo 到 之前实现的 Fiber 和调和步骤,

要让 memo 正常工作,需要完成几个步骤,具体如下:

  1. 识别 memo 组件:在 Fiber 树中,memo 组件会有特殊的标记(如 $$typeof: Symbol.for('react.memo'))。
  2. 比较 props:在调和过程中,当遇到 memo 组件时,使用其比较函数(自定义的或默认的浅比较)来判断 props 是否发生变化。
  3. 决定是否跳过渲染:如果 props 没有变化,则复用旧 Fiber,跳过子树的渲染;否则,进行正常的渲染和更新。

修改之前篇章的 reconcileChildren 函数以支持 memo

function reconcileChildren(fiber, children) {
  const oldFiberMap = {};
  let oldFiber = fiber.alternate ? fiber.alternate.child : null;

  // 构建旧 Fiber 的 key 映射
  while (oldFiber) {
    const key = oldFiber.key || oldFiber.index;
    oldFiberMap[key] = oldFiber;
    oldFiber = oldFiber.sibling;
  }

  let prevSibling = null;
  let newFiber = null;

  for (let index = 0; index < children.length; index++) {
    const child = children[index];
    const key = child.key || index;
    const matchedOldFiber = oldFiberMap[key];

    if (matchedOldFiber) {
      // 检查是否是 memo 组件
      if (child.$$typeof === Symbol.for('react.memo')) {
        const { type: Component, props, compare } = child;
        
        // 获取旧的 props
        const oldProps = matchedOldFiber.props;
        const areEqual = compare(oldProps, props);
        
        if (areEqual) {
          // Props 没有变化,复用旧 Fiber,跳过渲染
          newFiber = matchedOldFiber;
          newFiber.alternate = matchedOldFiber;
          newFiber.effectTag = null; // 清除旧的 effectTag
        } else {
          // Props 发生变化,创建新的 Fiber
          newFiber = createWorkInProgress(matchedOldFiber, props);
          newFiber.type = Component;
          newFiber.key = key;
          newFiber.effectTag = 'UPDATE';
        }
      } else {
        // 普通组件,复用旧 Fiber 并标记为更新
        const type = typeof child === 'string' || typeof child === 'number' ? TEXT_ELEMENT : child.type;
        newFiber = createWorkInProgress(matchedOldFiber, typeof child === 'object' ? child.props : { nodeValue: child });
        newFiber.type = type;
        newFiber.key = key;
        newFiber.effectTag = 'UPDATE';
      }

      delete oldFiberMap[key];
    } else {
      // 创建新的 Fiber
      const type = typeof child === 'string' || typeof child === 'number' ? TEXT_ELEMENT : child.type;
      newFiber = child.$$typeof === Symbol.for('react.memo') 
        ? {
            type: child.type.type, // 原始组件类型
            props: child.props,
            $$typeof: child.$$typeof,
            compare: child.compare || shallowCompare,
            key,
            effectTag: 'PLACEMENT',
            stateNode: createDom(child),
            hooks: [],
            currentHook: 0,
            alternate: null,
            contexts: new Map(),
            parent: fiber,
            child: null,
            sibling: null,
            effects: []
          }
        : new FiberNode(type, typeof child === 'object' ? child.props : { nodeValue: child }, fiber);
      
      if (child.$$typeof === Symbol.for('react.memo')) {
        newFiber.effectTag = 'PLACEMENT';
        newFiber.stateNode = createDom(newFiber);
      } else {
        newFiber.effectTag = 'PLACEMENT';
        newFiber.stateNode = createDom(newFiber); // 初始化 DOM 节点
      }
    }

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
  }

  // 标记旧节点为删除
  Object.values(oldFiberMap).forEach(fiberToDelete => {
    fiberToDelete.effectTag = 'DELETION';
  });
}

说明:

  • 识别 memo 组件:通过检查 child.$$typeof === Symbol.for('react.memo') 来识别是否是 memo 组件。

  • 比较 props:如果是 memo 组件,使用其 compare 函数(自定义或默认的浅比较)来判断 props 是否变化。

  • 复用或更新

    • 复用:如果 props 没有变化,复用旧的 Fiber,并跳过子树的渲染 (标记fiber 的 effectTag null)。
    • 更新:如果 props 发生变化,创建新的 Fiber,并标记为 'UPDATE',进行正常的渲染和更新。

这篇我们尝试实现了一个memo,以及讲解了它的底层实现原理。

下一篇,我们将从0-1完成 createPortal

如果文章对你有帮助,请点个赞支持一下!

啥也不是,散会。