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



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


在這種簡單的情景下,只要利用好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的區別:


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))
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毫無關聯
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形成耦合關系,兩者互相影響
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()“私人定制”那般隨心所欲
具體代碼就不放了
【完】--喜歡這篇文章的話不妨關注一下我喲