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模板方面的博客,過了兩天居然在今日頭條的推薦欄裏面看到了如出一轍的一篇文章,連文中使用的圖片都是徹底同樣(這個侵權的博主是誰這裏就不透露了,他發的文章、關注者還挺多,只能表示呵呵了~)。和這位「光明磊落」的博主進行溝通,獲得的倒是:「什麼你的個人,我看到了就是個人」這樣的回覆。真是天下之大,無奇不有,果斷向平臺提交了侵權投訴。而後該博主又舔着臉求放過,否則號要被封了,可真是可笑呢...

這篇文章就先到這裏啦,畢竟還處於自學階段,不少理解還不是很全面,文中如有不足之處,歡迎各位看官大大們的指正

看完點個贊吧,謝謝~

相關文章
相關標籤/搜索