细读 React | PureComponet

简介: 今天来聊一聊 React.Component、React.PureComponent、React.memo 的一些区别以及使用场景。

前言


今天来聊一聊 React.ComponentReact.PureComponentReact.memo 的一些区别以及使用场景。


正文


一、类组件定义


在 React 中,可以通过继承 React.ComponentReact.PureComponent 来定义 Class 组件:

import React, { Component, PureComponent } from 'react'
class Comp extends Component {
  // ...
}
class PureComp extends PureComponent {
  // ...
}


两者很相似,区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 propstate 的方式来实现了该函数。

如果赋予 React 组件相同的 propsstaterender() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。


注意:React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 propsstate 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。


二、浅层对比实现


我们来看下源码,它们是如何“浅层对比”的?


首先,在非强制更新组件的情况下,若 propsstate 的变更,内部都会触发 checkShouldComponentUpdate 方法来判断是否重新渲染组件。若使用 forceUpdate() 强制更新组件的话,则会跳过该方法。

// checkHasForceUpdateAfterProcessing 方法用于判断是否强制更新
// 若不是强制更新,则会根据 checkShouldComponentUpdate 方法判断是否应该更新组件
var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);

function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
  var instance = workInProgress.stateNode;
  // 若自实现了 shouldComponentUpdate 方法,则不会跑到后面的步骤
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');
    var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);
    stopPhaseTimer();
    {
      !(shouldUpdate !== undefined) ? warningWithoutStack$1(false, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', getComponentName(ctor) || 'Component') : void 0;
    }
    return shouldUpdate;
  }
  // 关键是这里:
  // 在 React 组件未实现 shouldComponentUpdate 前提下,
  // 可通过 isPureReactComponent 判断是否为 PureComponent 组件的原因是构造函数里设置了该属性的值为 true。
  // 使用 shallowEqual 方法来判断组件属性和状态时是否发生了变化,若两种均是“相等”,则返回 false,即不更新组件,否则会触发组件的 render() 方法以更新组件。
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
  }
  return true;
}


再看下 shallowEqual 的实现,不难:

function shallowEqual(objA, objB) {
  // is$1 相当于 ES6 的 Object.is() 方法,比较两个操作数是否相等
  if (is$1(objA, objB)) {
    return true;
  }
  // 讲过上一步的排除之后,若 objA 或 ObjB 的值是“非引用类型”或 null,则可以确定 objA 与 objB 是不相等的。
  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }
  // 走到这步,说明 objA 和 objB 是两个不同的引用类型的值
  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);
  // 比较两者的属性数量是否一致,若不一致,则可确定两者是不相等的
  if (keysA.length !== keysB.length) {
    return false;
  } // Test for A's keys different from B.
  // 这里只遍历最外层的属性是否一致
  for (var i = 0; i < keysA.length; i++) {
    // hasOwnProperty$2 即 Object.prototype.hasOwnProperty;
    // 先比较 objA 的属性,在 objB 属性有没有,若无说明两者不相等,否则接着再判断同一属性值是否相等,
    // 这判断就比较简单了:Object.is() 是使用全等判断的,并认为 NaN === NaN 和 +0 !== -0 的。
    if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  }
  // 否则,返回 true,认为它们相等。
  return true;
}


默认浅层对比方法,相当于:

shouldComponentUpdate(nextProps, nextState) {
  return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}


根据以上源码的分析,可以得出结论:


  • 若基于 React.PureComponent 的组件自实现了 shouldComponentUpdate() 方法,则会跳过默认的“浅层对比”,可以理解为覆盖了默认的 shouldComponentUpdate() 方法。。
  • 从源码可知,React.Component “未实现” shouldComponentUpdate() 是因为内部返回了 true 而已。
  • React.PureComponent 的浅层对比,主要分为三步判断:1️⃣ 对比 oldPropsnewProps 是否相等,若相等则返回 false,否则继续往下走;2️⃣ 接着判断 oldPropsnewProps (此时可以确定两者是不相等的引用值了)的第一层属性,若属性数量或者属性 key 不一致,则认为两者不相等并返回 true,否则继续往下走;3️⃣ 判断对应属性的属性值是否相等,若存在不相等则返回 true,否则返回 false。对于 oldStatenewState 的判断同理。
    注意:这里提到的返回值 true/false 是指 !shalldowEqual() 的结果,相当于 shouldComponentUpdate() 的返回值


三、示例及注意事项


基于以上结论,来看几个示例吧。


先明确几点:

  • 使用 setState() 来更新状态,无论状态值是否真的发生了改变,都会产生一个全新的对象,即 oldState !== newState
  • 组件的 props 对象是 readonly(只读)的,React 会保护它不被更改,否则会出错。
  • 每次父组件的重新渲染,子组件的 props 都会是一个全新的对象,即 oldProps !== newProps
  • 一般情况下,组件实例的 props 值几乎都是一个引用类型的值,即对象,我还没想到有什么场景会出现 null 的情况。而组件实例的 state 值则可能是对象或 null,后者即无状态的类组件,当然这种情况下应可能使用函数组件。

// 父组件
class Parent extends React.Component {
  state = {
    number: 0, // 原始类型
    list: [] // 引用类型
  }
  changeList() {
    const { list } = this.state
    list.push(0)
    this.setState({ list })
  }
  changeNumber() {
    this.setState({ number: this.state.number + 1 })
  }
  render() {
    console.log('---> Parent Render.')
    return (
      <>
        <button onClick={this.changeNumber.bind(this)}>Change Number</button>
        <button onClick={this.changeList.bind(this)}>Change List</button>
        <Child num={this.state.number} lists={this.state.list} />
      </>
    )
  }
}
// 子组件
class Child extends React.PureComponent {
  state = {
    name: 'child'
  }
  render() {
    console.log('---> Child Render.')
    return (
      <>
        <div>Child Component.</div>
      </>
    )
  }
}


1️⃣ 当我们点击父组件的 Change Number 按钮时,子组件会重新渲染。因为在对比子组件的 oldProps.numnewProps.num 时,两者的值不相等,因此会更新组件。在控制台可以看到:

---> Parent Render.
---> Child Render.


2️⃣ 当我们点击父组件的 Change List 按钮时,子组件不会重新渲染。因为在对比子组件的 oldProps.listnewProps.list 时,它们都是引用类型,且两者在内存中的地址是一致的,而且不会更深层次地去比较了,因此 React 认为它俩是相等的,因此不会更新组件。在控制台只看到:

---> Parent Render.


当然,这一点也是 React.PureComponent 的局限性,因此它应该应用于一些数据结构较为简单的展示类组件。


另外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。


四、延伸 React.memo


如果在函数组件中,想要拥有类似 React.PureComponent 的性能优化,可以使用 React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
})


React.memo高阶组件


如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext 的 Hook,当 context 发生变化时,它仍会重新渲染。


默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual)


此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。


注意,与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

简单来说,若需要更新组件,那么 areEqual 方法请返回 false,否则返回 true


The end.


目录
相关文章
|
前端开发 JavaScript 算法
细读 React | 元素、组件、实例
细读 React | 元素、组件、实例
350 0
细读 React | 元素、组件、实例
|
存储 自然语言处理 前端开发
细读 React | Refs
细读 React | Refs
236 0
细读 React | Refs
|
前端开发 JavaScript
细读 React | Fragment
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
236 0
|
前端开发 JavaScript API
细读 React | setState
今天来细聊一下 React 中的 setState()
210 0
|
7月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
382 0
|
7月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
80 0
|
7月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
83 0
|
7月前
|
存储 前端开发 JavaScript
【第29期】一文学会用React类组件编写组件
【第29期】一文学会用React类组件编写组件
81 0
|
7月前
|
前端开发 开发者
【第26期】一文读懂React组件编写方式
【第26期】一文读懂React组件编写方式
68 0
|
7月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
132 0