react 学习(14)实现 React.memo

459 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用法和实现原理。

memo 示例

这里我们接着上一小节的实现,添加函数组件

// src/index.js

function FunctionCounter(props) {
  console.log('FunctionCounter render')
  return <div>FunctionCount: {props.count}</div>
}
// memo 使用方式
const MemoFunctionCounter = React.memo(FunctionCounter)

...
render() {
  return <div>
    <MemoFunctionCounter count={this.state.number} />
    <ClassCounter count={this.state.number} />
    <input defaultValue={1} ref={this.amountRef} />
    <button onClick={this.handleClick}>+</button>
  </div>
}
...

我们打印下 memo 返回的是什么:

可以看到返回了一个 react 元素,元素类型是 react.memotype 对应我们传入的函数组件,compare 对应属性的判断方式,默认值就是类组件中的 shallowEqual 方法进行浅比较,因为函数组件中没有状态,所以只考虑属性。

前面我们提到过,react 元素就是一个对象,所以这里同样我们要对组件的挂在和更新进行处理,就跟 ProviderConsumer 处理一样的。说到这里相信跟下来的小伙伴脑袋里已经有了大概的思路。

memo 实现

  1. 首先 constants.js 添加新的元素类型
// src/constants.js
export const REACT_MEMO = Symbol('react.mome')
  1. 导出的 react 中添加方法
// src/react.js
// 从打印得知返回一个对象
function memo(type, compare = null) {
  return {
    $$typeof: REACT_MEMO,
    compare,
    type, // 传入的函数组件
  }
}

理论上这里就已经实现了 memo 方法,但是我们还要对组件的挂载和更新进行判断处理

  1. memo 类型挂载处理
// src/react-dom.js  

//createDOM
...
if (type && type.$$typeof === REACT_MEMO) {
  return mountMemoComponent(vdom)
}
...


function mountMemoComponent(vdom) {
  // 这里的 vdom 的 memo 方法返回的自身 vdom,即  <MemoFunctionCounter count={this.state.number}
  // type 对应的就是我们打印出来的值,type.type 就是我们传入的 函数组件,大家可以打印自行查看
  const {type: { type: FunctionCounter }, props} = vdom
  // 这里记录一下旧的属性
  vdom.prevProps = props
  // 执行我们传入的函数组件返回要渲染的 vdom
  let renderVdom = FunctionCounter(props)
  vdom.oldRenderVdom = renderVdom
  return createDOM(renderVdom)
}
  1. memo 类型更新处理
// src/react-dom.js 
// updateElement
...
if (oldVdom.type.$$typeof === REACT_MEMO) {
  updateMemoComponent(oldVdom, newVdom)
}
...


function updateMemoComponent(oldVdom, newVdom) {
  let { type: {compare}, prevProps } = oldVdom
  compare = compare || shallowEqual // 默认值
  
  if(compare(prevProps, newVdom.props)) {
    // 如果属性相同,不用更新,把oldVdom属性复制到newVdom
    newVdom.prevProps = prevProps
    newVdom.oldRenderVdom = oldVdom.oldRenderVdom
  } else {
    // 需要更新
    let oldDOM = findDOM(oldVdom)
    let parentDOM = oldDOM.parentNode
    
    const {type: {type: FunctionCounter}, props} = newVdom
    // 使用新的 vdom 渲染
    let renderVdom = FunctionCounter(props)
    // diff 对比,比较的是真实要渲染的 dom,不是直接的 oldVdom
    compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom)
    // 同理赋值
    newVdom.prevProps = props
    newVdom.oldRenderVdom = renderVdom
  }
}

使用我们自己的库执行,发现会有个小报错:

错误的原因是因为我们没有对空值进行过滤,导致 diff 对比时获取不到属性,我们做下简单的处理:

// src/react-dom.js

// updateChildren
...
// 我们对数组进行下 filter 过滤
oldVChildren = (Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren]).filter(el => typeof el !== 'undefined' && el !== null);

newVChildren = (Array.isArray(newVChildren) ? newVChildren : [newVChildren]).filter(el => typeof el !== 'undefined' && el !== null);
...

再次点击按钮执行,可以看到效果和原生库一样。这两节内容都是讲 react 针对优化渲染内部做的处理,大家可以对比着看。当然如果工作中要求不是很高,也可以忽略,因为 IE 已经淘汰了,现有的主流浏览器性能都很 ok。如有错误欢迎指正!