乐趣区

关于前端:面试官React怎么做性能优化

前言

最近始终在学习对于 React 方面的常识,并有幸正好失去一个机会将其用在了理论的我的项目中。所以我打算以博客的模式,将我在学习和开发(React)过程中遇到的问题记录下来。

这两天遇到了对于组件不必要的反复渲染问题,看了很多遍官网文档以及网上各位大大们的介绍,上面我会通过一些 demo 联合本人的了解进行汇总,并以此作为学习 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 组件从新渲染的起因)。

援用一段来自官网的形容:

当 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 组件中增加以下代码:

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

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

这里有个留神点就是,咱们从父组件 Parent 向子组件 Child 传递的是根本类型的数据,若传递的是援用类型的数据,咱们就须要在 shouldComponentUpdate 函数中进行深层比拟。但这种形式是十分影响效率,且会侵害性能的。所以咱们在传递的数据是根本类型是能够思考应用这种形式(即:this.props.son !== nextProps.son)进行性能优化。

(对于根本类型数据和援用类型数据的介绍,能够参考一下这篇文章:传送门)

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 模板方面的博客,过了两天居然在今日头条的举荐栏外面看到了截然不同的一篇文章,连文中应用的图片都是齐全一样(这个侵权的博主是谁这里就不走漏了,他发的文章、关注者还挺多,只能示意呵呵了~)。和这位“光明正大”的博主进行沟通,失去的却是:“什么你的我的,我看到了就是我的”这样的回复。真是天下之大,无奇不有,果决向平台提交了侵权投诉。而后该博主又舔着脸求放过,不然号要被封了,可真是好笑呢 …(负能量完结 今天又是美妙的一天

这篇文章就先到这里啦,毕竟还处于自学阶段,很多了解还不是很全面,文中若有不足之处,欢送各位看官大大们的斧正

退出移动版