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

 

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

 
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仍然沒有任何變化,但此時控制檯已經沒有任何輸出了,沒有意義的重渲染被咱們阻止了!java

 

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

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的數組元素,從而使父組件重渲染,繼而致使子組件重渲染git

demo:(點擊前)es6

點擊1後:github

控制檯輸出:npm

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()「私人定製」那般爲所欲爲

具體代碼就不放了

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

相關文章
相關標籤/搜索