手写React memo,理解memo原理

233 阅读2分钟

一. memo方法介绍

memo方法接收两个参数,第一个参数是组件方法Component,第二个参数是比对props方法comparememo方法返回ReactElement对象的type属性值

例如下面这段代码,通过memo方法缓存HelloWorld组件方法,当点击App组件第一个h1标签会修改text属性值,触发更新渲染,当渲染HelloWorld组件时,会比对props是否相同,因为count值没有变更,所以比对结果是相同的,所以不会调用HelloWorld组件方法,即控制台不会输出console.log,而是直接返回上次渲染结果。

当点击第二个h1标签时会修改count属性值,触发重新渲染,当渲染HelloWorld组件时,比对props不相同,重新调用HelloWorld组件方法获取新的child ReactElement,即控制台会输出console.log

const HelloWorld = memo(function HelloWorld({ count }: { count: number }) {
  console.log(count)
  
  return <h1>{count}</h1>
})

function App() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  return (
    <div>
      <h1 onClick={() => setText('are you ok?')}>text click</h1>
      <h1 onClick={() => setCount(count + 1)}>count click</h1>
      <h1>Text: {text}</h1>
      <HelloWorld count={count} />
    </div>
  )
}

二. 实现memo

强烈推荐阅读手写mini React,理解React渲染原理,有助于理解本文章内容

2.1 定义memo方法

创建elementType对象,$$typeof属性记录ReactElement对象类型,type属性记录函数组件方法,compare属性记录比对props方法,创建的elementType对象会作为ReactElement对象的type属性值

function memo(Component, compare = null) {
  const elementType = {
    $$typeof: REACT_MEMO_TYPE, // Symbol.for('react.memo')
    type: Component, // 记录函数组件方法
    compare, // 记录比对props方法
  }
  return elementType
}

以这段代码为例const HelloWorld = memo(function HelloWorld({ count }) {}),创建的ReactElement对象结构如下

{
  $$typeof: Symbol.for('react.transitional.element'),
  key: null,
  props: { count: 0 },
  type: {
    $$typeof: Symbol.for('react.memo'),
    type: function HelloWorld({ count }) {},
    compare: null,
  },
}

2.2 创建ReactElement对象对应的FiberNode节点

根据ReactElement对象的type属性创建对应的FiberNode节点,FiberNode节点的tag属性值为MemoComponent

function createFiberFromElement(element) {
  let fiberTag
  const { type } = element
  if (typeof type === 'function') {
    fiberTag = FunctionComponent
  } else if (typeof type === 'string') {
    fiberTag = HostComponent
  } else {
    // memo类型FiberNode节点
    switch (type.$$typeof) {
      case REACT_MEMO_TYPE:
        fiberTag = MemoComponent
        break
    }
  }
  const fiber = new FiberNode(fiberTag, element.props)
  fiber.key = element.key
  fiber.elementType = type
  coerceRef(fiber, element)
  return fiber
}

2.3 调用组件方法

在构建虚拟DOM树阶段,递归遍历到MemoComponent类型的FiberNode节点

  • 判断旧FiberNode节点是否存在,如果存在则比对新旧节点prop是否相同,相同则复用旧child FiberNode
  • 如果旧FiberNode节点不存在或比对props不相同,则调用组件方法获取新的child ReactElement
function updateMemoComponent(current, workInProgress) {
  if (current !== null) {
    // 获取比对props方法
    const compare = workInProgress.elementType.compare || shallowEqual
    // 获取旧属性值
    const prevProps = current.pendingProps
    // 获取新属性值
    const nextProp = workInProgress.pendingProps
    // 比对属性值是否相同,相同复用旧child FiberNode节点
    if (compare(prevProps, nextProp)) {
      return cloneChildFibers(current, workInProgress)
    }
  }
  // 获取组件方法
  const Component = workInProgress.elementType.type
  // 调用组件方法获取新的child ReactElement
  return updateFunctionComponent(current, workInProgress, Component)
}

function beginWork(workInProgress) {
  // 获取旧FiberNode节点
  const current = workInProgress.alternate
  switch (workInProgress.tag) {
    case MemoComponent:
      return updateMemoComponent(current, workInProgress)
  }
}

2.3.1 比对props默认方法

通过Object.is比对属性值是否相同

function shallowEqual(objA, objB) {
  if (Object.is(objA, objB)) return true
  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++) {
    const currentKey = keysA[i]
    if (
      !Object.prototype.hasOwnProperty.call(objB, currentKey) ||
      !Object.is(objA[currentKey], objB[currentKey])
    )
      return false
  }
  return true
}

三. 往期文章推荐

3.1 React原理系列总结

四. 参考文档

4.1 React memo官方文档