React性能优化之shouldComponentUpdate、PureComponent和React.memo

前言

最近一直在学习关于React方面的知识,并有幸正好获得一个机会将其用在了实际的项目中。因此我打算以博客的形式,将我在学习和开发(React)过程当中遇到的问题记录下来。html

这两天遇到了关于组件没必要要的重复渲染问题,看了不少遍官方文档以及网上各位大大们的介绍,下面我会经过一些demo结合本身的理解进行汇总,并以此做为学习React的第一篇笔记(本身学习,什么都好,就是费头发...)。vue

本文主要介绍如下三种优化方式(三种方式有着类似的实现原理):react

  • shouldComponentUpdate
  • React.PureComponent
  • React.memo

其中shouldComponentUpdateReact.PureComponent是类组件中的优化方式,而React.memo是函数组件中的优化方式。编程

引出问题

  1. 新建Parent类组件。
import React, { Component } from 'react'
import Child from './Child'

class Parent extends Component {
  constructor(props) {
    super(props)
    this.state = {
      parentInfo: 'parent',
      sonInfo: 'son'
    }
    this.changeParentInfo = this.changeParentInfo.bind(this)
  }

  changeParentInfo() {
    this.setState({
      parentInfo: `改变了父组件state:${Date.now()}`
    })
  }

  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child son={this.state.sonInfo}></Child>
      </div>
    )
  }
}

export default Parent

复制代码
  1. 新建Child类组件。
import React, {Component} from 'react'

class Child extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:
        <p>{this.props.son}</p>
      </div>
    )
  }
}

export default Child

复制代码
  1. 打开控制台,咱们能够看到控制台中前后输出了Parent Component renderChild Component render
    点击按钮,咱们会发现又输出了一遍Parent Component renderChild Component render
    点击按钮时咱们只改变了父组件Parentstate中的parentInfo的值,Parent更新的同时子组件Child也进行了从新渲染,这确定是咱们不肯意看到的。因此下面咱们就围绕这个问题介绍本文的主要内容。

shouldComponentUpdate

React提供了生命周期函数shouldComponentUpdate(),根据它的返回值(true | false),判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会从新渲染(这也就说明了上面👆Child组件从新渲染的缘由)。api

引用一段来自官网的描述:数组

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行以前被调用。返回值默认为 true。目前,若是shouldComponentUpdate返回 false,则不会调用UNSAFE_componentWillUpdate()render()componentDidUpdate()方法。后续版本,React 可能会将shouldComponentUpdate()视为提示而不是严格的指令,而且,当返回 false 时,仍可能致使组件从新渲染。缓存

shouldComponentUpdate方法接收两个参数nextPropsnextState,能够将this.propsnextProps以及this.statenextState进行比较,并返回 false 以告知 React 能够跳过更新。性能优化

shouldComponentUpdate (nextProps, nextState) {
  return true
}
复制代码

此时咱们已经知道了shouldComponentUpdate函数的做用,下面咱们在Child组件中添加如下代码:bash

shouldComponentUpdate(nextProps, nextState) {
    return this.props.son !== nextProps.son
}
复制代码

这个时候再点击按钮修改父组件 state 中的parentInfo的值时,Child组件就不会再从新渲染了。数据结构

这里有个注意点就是,咱们从父组件Parent向子组件Child传递的是基本类型的数据,若传递的是引用类型的数据,咱们就须要在shouldComponentUpdate函数中进行深层比较。但这种方式是很是影响效率,且会损害性能的。因此咱们在传递的数据是基本类型是能够考虑使用这种方式进行性能优化。

(关于基本类型数据和引用类型数据的介绍,能够参考一下这篇文章:传送门

React.PureComponent

React.PureComponentReact.Component很类似。二者的区别在于React.Component并未实现 shouldComponentUpdate,而React.PureComponent中以浅层对比 prop 和 state 的方式来实现了该函数。

Child组件的内容修改成如下内容便可,这是否是很方便呢。

import React, { PureComponent } from 'react'

class Child extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
    }
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:
        <p>{this.props.son}</p>
      </div>
    )
  }
}

export default Child

复制代码

因此,当组件的 props 和 state 均为基本类型时,使用React.PureComponent能够起到优化性能的做用。

若是对象中包含复杂的数据结构,则有可能由于没法检查深层的差异,产生错误的比对结果。

为了更好的感觉引用类型数据传递的问题,咱们先改写一下上面的例子:

  • 修改Child组件。
import React, {Component} from 'react'

class Child extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.props.parentInfo !== nextProps.parentInfo
  }

  updateChild () {
    this.forceUpdate()
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:
        <p>{this.props.parentInfo[0].name}</p>
      </div>
    )
  }
}

export default Child

复制代码
  • 修改Parent组件。
import React, { Component } from 'react'
import Child from './Child'

class Parent extends Component {
  constructor(props) {
    super(props)
    this.state = {
      parentInfo: [
        { name: '哈哈哈' }
      ]
    }
    this.changeParentInfo = this.changeParentInfo.bind(this)
  }

  changeParentInfo() {
    let temp = this.state.parentInfo
    temp[0].name = '呵呵呵:' + new Date().getTime()
    this.setState({
      parentInfo: temp
    })
  }

  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo[0].name}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child parentInfo={this.state.parentInfo}></Child>
      </div>
    )
  }
}

export default Parent

复制代码

此时在控制台能够看到,ParentChild都进行了一次渲染,显示的内容是一致的。

点击按钮,那么问题来了,如图所示,父组件Parent进行了从新渲染,从页面上咱们能够看到,Parent组件中的parentInfo确实已经发生了改变,而子组件却没有发生变化。

因此当咱们在传递引用类型数据的时候,shouldComponentUpdate()React.PureComponent存在必定的局限性。

针对这个问题,官方给出的两个解决方案:

  • 在深层数据结构发生变化时调用forceUpdate()来确保组件被正确地更新(不推荐使用);
  • 使用immutable对象加速嵌套数据的比较(不一样于深拷贝);

forceUpdate

当咱们明确知道父组件Parent修改了引用类型的数据(子组件的渲染依赖于这个数据),此时调用forceUpdate()方法强制更新子组件,注意,forceUpdate()会跳过子组件的shouldComponentUpdate()

修改Parent组件(将子组件经过ref暴露给父组件,在点击按钮后调用子组件的方法,强制更新子组件,此时咱们能够看到在父组件更新后,子组件也进行了从新渲染)。

{
  ...
  changeParentInfo() {
    let temp = this.state.parentInfo
    temp[0].name = '呵呵呵:' + new Date().getTime()
    this.setState({
      parentInfo: temp
    })
    this.childRef.updateChild()
  }
  
  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo[0].name}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child ref={(child)=>{this.childRef = child}} parentInfo={this.state.parentInfo}></Child>
      </div>
    )
  }
}

复制代码

immutable

Immutable.js是 Facebook 在 2014 年出的持久性数据结构的库,持久性指的是数据一旦建立,就不能再被更改,任何修改或添加删除操做都会返回一个新的 Immutable 对象。可让咱们更容易的去处理缓存、回退、数据变化检测等问题,简化开发。而且提供了大量的相似原生 JS 的方法,还有 Lazy Operation 的特性,彻底的函数式编程。

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否须要执行 render(),而这个操做几乎 0 成本,因此能够极大提升性能。首先将Parent组件中调用子组件强制更新的代码this.childRef.updateChild()进行注释,再修改Child组件的shouldComponentUpdate()方法:

import { is } from 'immutable'

shouldComponentUpdate (nextProps = {}, nextState = {}) => {
  return !(this.props === nextProps || is(this.props, nextProps)) ||
      !(this.state === nextState || is(this.state, nextState))
}
复制代码

此时咱们再查看控制台和页面的结果能够发现,子组件进行了从新渲染。

关于shouldComponentUpdate()函数的优化,上面👆的方法还有待验证,仅做为demo使用,实际的开发过程当中可能须要进一步的探究选用什么样的插件,什么样的判断方式才是最全面、最合适的。若是你们有好的建议和相关的文章欢迎砸过来~

React.memo

关于React.memo的介绍,官网描述的已经很清晰了,这里我就直接照搬了~

React.memo 为高阶组件。它与 React.PureComponent 很是类似,但只适用于函数组件,而不适用 class 组件。

若是你的函数组件在给定相同 props 的状况下渲染相同的结果,那么你能够经过将其包装在 React.memo 中调用,以此经过记忆组件渲染结果的方式来提升组件的性能表现。这意味着在这种状况下,React 将跳过渲染组件的操做并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变动。若是函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会从新渲染。

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

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

使用函数组件改写一下上面的例子:

Child组件:

import React, {useEffect} from 'react'
// import { is } from 'immutable'

function Child(props) {

  useEffect(() => {
    console.log('Child Component')
  })

  return (
    <div>
      这里是child子组件:
      <p>{props.parentInfo[0].name}</p>
    </div>
  )
}

export default Child

复制代码

Parent组件:

import React, {useEffect, useState} from 'react'
import Child from './Child'

function Parent() {

  useEffect(() => {
    console.log('Parent Component')
  })

  const [parentInfo, setParentInfo] = useState([{name: '哈哈哈'}])
  const [count, setCount] = useState(0)

  const changeCount = () => {
    let temp_count = count + 1
    setCount(temp_count)
  }
  return (
    <div>
      <p>{count}</p>
      <button onClick={changeCount}>改变父组件state</button>
      <br/>
      <Child parentInfo={parentInfo}></Child>
    </div>
  )
}

export default Parent

复制代码

运行程序后,和上面的例子进行同样的操做,咱们会发现随着父组件count的值的修改,子组件也在进行重复渲染,因为是函数组件,因此咱们只能经过React.memo高阶组件来跳过没必要要的渲染。

修改Child组件的导出方式:export default React.memo(Child)

再运行程序,咱们能够看到父组件虽然修改了count的值,但子组件跳过了渲染。

这里我用的是React hooks的写法,在hooks中useState修改引用类型数据的时候,每一次修改都是生成一个新的对象,也就避免了引用类型数据传递的时候,子组件不更新的状况。


刚接触react,最大的感触就是它的自由度是真的高,全部的内容均可以根据本身的喜爱设置,但这也增长了初学者的学习成本。(不过付出和收获是成正比的,继续个人救赎之路!)

总结

  1. 类组件中:shouldComponentUpdate()React.PureComponent 在基本类型数据传递时均可以起到优化做用,当包含引用类型数据传递的时候,shouldComponentUpdate()更合适一些。
  2. 函数组件:使用 React.memo

另外吐槽一下如今的网上的部分“博客”,一堆重复(如出一辙)的文章。复制别人的文章也请本身验证一下吧,API变动、时代发展等因素引发的问题理解,可是连错别字,错误的使用方法都全篇照搬,而后文末贴一下别人的地址这就结束了???怕别人的地址失效,想保存下来?但这种方式不说误导别人,就说本身回顾的时候也会有问题吧,这是什么样的心态?

再说下上个月身边的真实例子。有个同事写了篇关于vue模板方面的博客,过了两天居然在今日头条的推荐栏里面看到了如出一辙的一篇文章,连文中使用的图片都是彻底同样(这个侵权的博主是谁这里就不透露了,他发的文章、关注者还挺多,只能表示呵呵了~)。和这位“光明磊落”的博主进行沟通,获得的倒是:“什么你的个人,我看到了就是个人”这样的回复。真是天下之大,无奇不有,果断向平台提交了侵权投诉。而后该博主又舔着脸求放过,否则号要被封了,可真是可笑呢...

这篇文章就先到这里啦,毕竟还处于自学阶段,不少理解还不是很全面,文中如有不足之处,欢迎各位看官大大们的指正

看完点个赞吧,谢谢~

相关文章
相关标签/搜索