【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()“私人定制”那般为所欲为

具体代码就不放了

【完】--喜欢这篇文章的话不妨关注一下我哟