【react】利用shouldComponentUpdate鈎子函數優化react性能以及引入immutable庫的必要性


 

凡是參閱過react官方英文文檔的童鞋大體上都能知道對於一個組件來說,其state的改變(調用this.setState()方法)以及從父組件接受的props發生變化時,會導致組件重渲染,正所謂"學而不思則罔",在不斷的學習中,我開始思考這一些問題:

 
1.setState()函數在任何情況下都會導致組件重渲染嗎?如果setState()中參數還是原來沒有發生任何變化的state呢?
2.如果組件的state沒有變化,並且從父組件接受的props也沒有變化,那它就一定不會重渲染嗎?
3.如果1,2兩種情況下都會導致重渲染,我們該如何避免這種冗余的操作,從而優化性能?
 
下面我就用實例一一探討這些問題:
沒有導致state的值發生變化的setState是否會導致重渲染 ——【會!】
 
import React from 'react'
class Test extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      Number:1//設state中Number值為1
    }
  }
  //這里調用了setState但是並沒有改變setState中的值
  handleClick = () => {
     const preNumber = this.state.Number
     this.setState({
        Number:this.state.Number
     })
  }
  render(){
    //當render函數被調用時,打印當前的Number
    console.log(this.state.Number)
    return(<h1 onClick = {this.handleClick} style ={{margin:30}}>
             {this.state.Number}
           </h1>)
  }
}
export default Test
//省略reactDOM的渲染代碼...
demo:
點擊1一共15次,其間demo沒有發生任何變化

 

控制台輸出:(我點擊了1一共15次  _(:3 」∠)_)
 
那么問題就來了,我的UI明明就沒有任何變化啊,為什么要做着中多余的重渲染的工作呢?把這工作給去掉吧!
 
於是這里react生命周期中的shouldComponentUpdate函數就派上用場了!shouldComponentUpdate函數是重渲染時render()函數調用前被調用的函數,它接受兩個參數:nextProps和nextState,分別表示下一個props和下一個state的值。並且,當函數返回false時候,阻止接下來的render()函數的調用,阻止組件重渲染,而返回true時,組件照常重渲染。
 
我們對上面的情況做一個小小的改動:
import React from 'react'
class Test extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      Number:1
    }
  }
  //這里調用了setState但是並沒有改變setState中的值
  handleClick = () => {
     const preNumber = this.state.Number
     this.setState({
        Number:this.state.Number
     })
  }
  //在render函數調用前判斷:如果前后state中Number不變,通過return false阻止render調用
  shouldComponentUpdate(nextProps,nextState){
      if(nextState.Number == this.state.Number){
        return false
      }
  }
  render(){
    //當render函數被調用時,打印當前的Number
    console.log(this.state.Number)
    return(<h1 onClick = {this.handleClick} style ={{margin:30}}>
             {this.state.Number}
           </h1>)
  }
}

 

點擊標題1,UI仍然沒有任何變化,但此時控制台已經沒有任何輸出了,沒有意義的重渲染被我們阻止了!

 

組件的state沒有變化,並且從父組件接受的props也沒有變化,那它就還可能重渲染嗎?——【可能!】

import React from 'react'
class Son extends React.Component{
  render(){
    const {index,number,handleClick} = this.props
    //在每次渲染子組件時,打印該子組件的數字內容
    console.log(number);
    return <h1 onClick ={() => handleClick(index)}>{number}</h1>
  }
}
class Father extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      numberArray:[0,1,2]
    }
  }
  //點擊后使numberArray中數組下標為index的數字值加一,重渲染對應的Son組件
  handleClick = (index) => {
     let preNumberArray = this.state.numberArray
     preNumberArray[index] += 1;
     this.setState({
        numberArray:preNumberArray
     })
  }
  render(){
    return(<div style ={{margin:30}}>{
              this.state.numberArray.map(
                (number,key) => {
                 return <Son
                           key = {key}
                           index = {key}
                           number ={number}
                           handleClick ={this.handleClick}/>
                }
                )
              }
           </div>)
  }
}
export default Father

 

在這個例子中,我們在父組件Father的state對象中設置了一個numberArray的數組,並且將數組元素通過map函數傳遞至三個子組件Son中,作為其顯示的內容(標題1,2,3),點擊每個Son組件會更改對應的state中numberArray的數組元素,從而使父組件重渲染,繼而導致子組件重渲染

demo:(點擊前)

點擊1后:

控制台輸出:

demo如我們設想,但這里有一個我們無法滿意的問題:輸出的(1,1,2),有我們從0變到1的數據,也有未發生變化的1和2。這說明Son又做了兩次多余的重渲染,但是對於1和2來說,它們本身state沒有變化(也沒有設state),同時父組件傳達的props也沒有變化,所以我們又做了無用功。

那怎么避免這個問題呢?沒錯,關鍵還是在shouldComponentUpdate這個鈎子函數上

 
import React from 'react'
class Son extends React.Component{
  shouldComponentUpdate(nextProps,nextState){
      if(nextProps.number == this.props.number){
        return false
      }
      return true
  }
  render(){
    const {index,number,handleClick} = this.props
    //在每次渲染子組件時,打印該子組件的數字內容
    console.log(number);
    return <h1 onClick ={() => handleClick(index)}>{number}</h1>
  }
}
class Father extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      numberArray:[0,1,2]
    }
  }
  //點擊后使numberArray中數組下標為index的數字值加一,重渲染對應的Son組件
  handleClick = (index) => {
     let preNumberArray = this.state.numberArray
     preNumberArray[index] += 1;
     this.setState({
        numberArray:preNumberArray
     })
  }
  render(){
    return(<div style ={{margin:30}}>{
              this.state.numberArray.map(
                (number,key) => {
                 return <Son
                           key = {key}
                           index = {key}
                           number ={number}
                           handleClick ={this.handleClick}/>
                }
                )
              }
           </div>)
  }
}
export default Father
 
這次只打印了數字發生改變的numberArray[0]對應的Son組件,說明numberArray[1],numberArray[2]的重渲染被“過濾”了!(goodjob!)
 
 
【注意】:nextProps.number == this.props.number不能寫成nextProps == this.props,它總返回false因為它們是堆中內存不同的兩個對象。(對比上面的紅色的【注意】)
 
 【總結】
一句話總結以上例子的結論:前后不改變state值的setState(理論上)和無數據交換的父組件的重渲染都會導致組件的重渲染,但你可以在shouldComponentUpdate這道兩者必經的關口阻止這種浪費性能的行為
 
 

 在這種簡單的情景下,只要利用好shouldComponent一切都很美好,但是當我們的state中的numberArray變得復雜些的時候就會遇到很有意思的問題了,讓我們把numberArray改成

[{number:0 /*對象中其他的屬性*/},
 {number:1 /*對象中其他的屬性*/},
 {number:2 /*對象中其他的屬性*/}
]
這種對象數組的數據形式,整體的代碼結構仍然不變:
import React from 'react'
class Son extends React.Component{
  shouldComponentUpdate(nextProps,nextState){
      if(nextProps.numberObject.number == this.props.numberObject.number){
        return false
      }
      return true
  }
  render(){
    const {index,numberObject,handleClick} = this.props
    //在每次渲染子組件時,打印該子組件的數字內容
    console.log(numberObject.number);
    return <h1 onClick ={() => handleClick(index)}>{numberObject.number}</h1>
  }
}
class Father extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      numberArray:[{number:0 /*對象中其他的屬性*/},
                   {number:1 /*對象中其他的屬性*/},
                   {number:2 /*對象中其他的屬性*/}
                   ]
    }
  }
  //點擊后使numberArray中數組下標為index的數字值加一,重渲染對應的Son組件
  handleClick = (index) => {
     let preNumberArray = this.state.numberArray
     preNumberArray[index].number += 1;
     this.setState({
        numberArray:preNumberArray
     })
  }
  render(){
    return(<div style ={{margin:30}}>{
              this.state.numberArray.map(
                (numberObject,key) => {
                 return <Son
                           key = {key}
                           index = {key}
                           numberObject ={numberObject}
                           handleClick ={this.handleClick}/>
                }
                )
              }
           </div>)
  }
}
export default Father
 
這個時候發現無論如何點擊三個標題均無變化(沒有數字改變),且控制台無輸出!

 what!!!我的代碼結構明明沒有任何變化啊,只是改傳遞數字為傳遞對象而已。嗯嗯,問題就出在這里,我們傳遞的是對象,關鍵在於nextProps.numberObject.number == this.props.numberObject.number這個判斷條件,讓我們思考,這與前面成功例子中的nextProps.number == this.props.number的區別:

1numberObject是一個對象
2.number是一個數字變量
3數字變量(number類型)和對象(Object類型)的內存存儲機制不同
 
javascript變量分為基本類型變量和引用類型變量
對於number,string,boolean,undefined,null這些基本類型變量,值存在棧中:
對於object,Array,function這些引用類型變量,引用存在棧中,而不同的引用卻可以指向堆內存中的同一個對象:
然后我們回過頭再去看剛才的問題,在上面,nextProps.numberObject和this.props.numberObject的實際上指向的是同一個堆內存中的對象,所以點擊標題時在多次判斷條件中nextProps.numberObject.number==this.props.numberObject.number 等同於0 == 0 --> 1 == 1--> 2 == 2,所以總返回true,導致每次點擊 調用shouldComponentUpdate()函數時都阻止了渲染,所以我們才看不到標題變化和控制台輸出。
怎么才能保證每次取到不同的numberObject?
 
我們有三種方式:
 
1.ES6的擴展語法Object.assign()//react官方推薦的es6寫法
2深拷貝/淺拷貝或利用JSON.parse(JSON.stringify(data))//相當於深拷貝,但使用受一定限制,具體的童鞋們可自行百度
3 immutable.js//react官方推薦使用的第三方庫,目前github上20K star,足見其火熱
4 繼承react的PureComponent組件
 
1ES6的擴展語法Object.assign()
 
object.assign(TargetObj,obj1,obj2 ...)[返回值為Oject]可將obj1,obj2等組合到TargetObj中並返回一個和TargetObj值相同的對象,比如
 let obj = object.assign({},{a:1},{b:1})//obj為{a:1,b:1}

 

import React from 'react'
class Son extends React.Component{
  shouldComponentUpdate(nextProps,nextState){
     //舊的number Object對象的number屬性 == 新的number Object對象的number屬性
      if(nextProps.numberObject.number == this.props.numberObject.number){
        console.log('前一個對象' + JSON.stringify(nextProps.numberObject)+
                    '后一個對象' + JSON.stringify(this.props.numberObject));
        return false
      }
      return true
  }
  render(){
    const {index,numberObject,handleClick} = this.props
    //在每次渲染子組件時,打印該子組件的數字內容
    console.log(numberObject.number);
    return <h1 onClick ={() => handleClick(index)}>{numberObject.number}</h1>
  }
}
class Father extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      numberArray:[{number:0 /*對象中其他的屬性*/},
                   {number:1 /*對象中其他的屬性*/},
                   {number:2 /*對象中其他的屬性*/}
                   ]
    }
  }
  //點擊后使numberArray中數組下標為index的數字值加一,重渲染對應的Son組件
  handleClick = (index) => {
     let preNumberArray = this.state.numberArray
     //把做修改的number Object先拷貝到一個新的對象中,替換原來的對象
     preNumberArray[index] = Object.assign({},preNumberArray[index])
     //使新的number Object對象的number屬性加一,舊的number Object對象屬性不變
     preNumberArray[index].number += 1;
     this.setState({numberArray:preNumberArray})
  }
  render(){
    return(<div style ={{margin:30}}>{
              this.state.numberArray.map(
                (numberObject,key) => {
                 return <Son
                           key = {key}
                           index = {key}
                           numberObject ={numberObject}
                           handleClick ={this.handleClick}/>
                }
                )
              }
           </div>)
  }
}
export default Father

 點擊0后打印1,問題解決!

2深拷貝/淺拷貝或利用JSON.parse(JSON.stringify(data))

在這里先不多介紹了,大家可自行百度...
 
3immutable.js —— react官方推薦的第三方庫:
先讓我們回到困擾我們的問題的根源 —— 兩個引用類型變量的賦值表達式和兩個基本類型變量的賦值表達式不同。
對於基本類型變量a和b, b = a 后,訪問a,b相當於訪問兩個不同的變量,兩者彼此毫無關聯
let a =2,b;
b = a;//將a的值賦給b
a = 1;//改變a的值
console.log('a =' + a);//輸出 a = 1
console.log('b =' + b);//輸出 b = 2,表明賦值后b,a毫無關聯
對於引用類型變量obj1和obj2,obj1 = obj2后,訪問obj1和obj2相當於訪問同一個變量,兩者形成了一種“耦合”的關系
let obj1 ={name:'李達康'},obj2;
obj2 = obj1;//將obj1的地址賦給obj2
obj1.name = '祁同偉';//改變obj1的name屬性值
console.log('obj1.name =' + obj1.name);//輸出 obj1.name = '祁同偉'
console.log('obj2.name =' + obj2.name);//輸出 obj2.name = '祁同偉',表明賦值后obj1/obj2形成耦合關系,兩者互相影響
 
為什么基本類型和引用類型在變量賦值上面存在這么大的不同呢?因為基本類型變量占用的內存很小,而引用類型變量占用的內存比較大,幾個引用類型變量通過指針共享同一個變量可以節約內存
 
所以,在這個例子中,我們上面和下面所做的一切,都是在消除對象賦值表達式所帶來的這一負面影響
 
那我們能不能通過一些方式,使得preNumberArray = this.state.numberArray的時候,兩變量指向的就是不同的兩個對象呢?於是這里就引入了一個強大的第三方庫 ——immutable.js,先舉個例子示范一下:
(首先要通過npm install immutable 安裝immutable的依賴包哦)
const { fromJS } = require('immutable')
let obj1 = fromJS({name:'李達康'}),obj2;
obj2 = obj1;//obj2取得與obj1相同的值,但兩個引用指向不同的對象
obj2 = obj2.set('name','祁同偉');//設置obj2的name屬性值為祁同偉
console.log('obj1.name =' + obj1.get('name'));//obj1.name =李達康
console.log('obj2.name =' + obj2.get('name'));//obj2.name =祁同偉

【注意】

1這個時候obj1=obj2並不會使兩者指向同一個堆內存中的對象了!所以這成功繞過了我們前面的所提到的對象賦值表達式所帶來的坑。所以我們可以隨心所欲地像使用普通基本類型變量復制 (a=b)那樣對對象等引用類型賦值(obj1 = obj2)而不用拷貝新對象

2對於immutable對象,你不能再用obj.屬性名那樣取值了,你必須使用immuutable提供的API

  • fromJS(obj)把傳入的obj封裝成immutable對象,在賦值給新對象時傳遞的只有本身的值而不是指向內存的地址。
  • obj.set(屬性名,屬性值)給obj增加或修改屬性,但obj本身並不變化,只返回修改后的對象
  • obj.get(屬性名)從immutable對象中取得屬性值

1優點:深拷貝/淺拷貝本身是很耗內存,而immutable本身有一套機制使內存消耗降到最低

2缺點:你多了一整套的API去學習,並且immutable提供的set,map等對象容易與ES6新增的set,map對象弄混

讓我們一試為快:
import React from 'react'
const { fromJS } = require('immutable')
class Son extends React.Component{
  shouldComponentUpdate(nextProps,nextState){
     //舊的number Object對象的number屬性 == 新的number Object對象的number屬性
      if(nextProps.numberObject.get('number') == this.props.numberObject.get('number')){
        return false
      }
      return true
  }
  render(){
    const {index,numberObject,handleClick} = this.props
    console.log(numberObject.get('number'));
    //在每次渲染子組件時,打印該子組件的數字內容
    return <h1 onClick ={() => handleClick(index)}>{numberObject.get('number')}</h1>
  }
}
class Father extends React.Component{
  constructor(props) {
    super(props);
    this.state = {
      numberArray:fromJS([{number:0 /*對象中其他的屬性*/},
                          {number:1 /*對象中其他的屬性*/},
                          {number:2 /*對象中其他的屬性*/}
                        ])
    }
  }
  //點擊后使numberArray中數組下標為index的數字值加一,重渲染對應的Son組件
  handleClick = (index) => {
     let preNumberArray = this.state.numberArray
     //使新的number Object對象的number屬性加一,舊的number Object對象屬性不變
     let newNumber = preNumberArray.get(index).get('number') + 1;
     preNumberArray = preNumberArray.set(index,fromJS({number: newNumber}));
     this.setState({numberArray:preNumberArray})
  }
  render(){
    return(<div style ={{margin:30}}>{
              this.state.numberArray.map(
                (numberObject,key) => {
                 return <Son
                           key = {key}
                           index = {key}
                           numberObject ={numberObject}
                           handleClick ={this.handleClick}/>
                }
                )
              }
           </div>)
  }
}
export default Father

成功,demo效果同上

 

這篇文章實在太過冗長,不過既然您已經看到這里了,那么我就介紹解決上述問題的一種簡單粗暴的方法——

4繼承react的PureComponent組件

如果你只是單純地想要避免state和props不變下的冗余的重渲染,那么react的pureComponent可以非常方便地實現這一點:

import React, { PureComponent } from 'react'
class YouComponent extends PureComponent {
render() {
// ...
}
}

當然了,它並不是萬能的,由於選擇性得忽略了shouldComponentUpdate()這一鈎子函數,它並不能像shouldComponentUpdate()“私人定制”那般隨心所欲

具體代碼就不放了

【完】--喜歡這篇文章的話不妨關注一下我喲


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM