很多人在寫React元件的時候沒有太在意React元件的效能,使得React做了很多不必要的render,現在我就說說該怎麼來編寫搞效能的React元件。
首先我們來看一下下面兩個元件
import React, {PureComponent,Component} from "react" import PropTypes from "prop-types" class A extends Component { constructor(props){ super(props); } componentDidUpdate() { console.log("componentDidUpdate") } render (){ return ( <div /> ) } } class Test extends Component { constructor(props) { super(props); this.state={ value:0 }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(()=>{ this.setState({value:this.state.value+1}) },100); } render() { return ( <A /> ) } }
執行結果:
Test state change. A componentDidUpdate
我們發現上面程式碼中只要執行了Test元件的中的setState,無論Test元件裡面包含的子元件A是否需要這個state裡面的值,A componentDidUpdate始終會輸出
試想下如果子元件下面還有很多子元件,元件又巢狀子元件,子子孫孫無窮盡也,這是不是個很可怕的效能消耗?
當然,針對這樣的一個問題最初的解決方案是通過shouldComponentUpdate方法做判斷更新,我們來改寫下元件A
class A extends Component { constructor(props){ super(props); } static propTypes = { value:PropTypes.number }; static defaultProps = { value:0 }; shouldComponentUpdate(nextProps, nextState) { return nextProps.value !== this.props.value; } componentDidUpdate() { console.log("A componentDidUpdate"); } render (){ return ( <div /> ) } }
這裡增加了shouldComponentUpdate方法來對傳入的value屬性進行對面,雖然這裡沒有傳,但是不影響,執行結果:
Test state change.
好了,這次結果就是我們所需要的了,但是如果每一個元件都這樣做一次判斷是否太過於麻煩?
那麼React 15.3.1版本中增加了 PureComponent ,我們來改寫一下A元件
class A extends PureComponent { constructor(props){ super(props); } static propTypes = { value:PropTypes.number }; static defaultProps = { value:0 }; componentDidUpdate() { console.log("A componentDidUpdate"); } render (){ return ( <div /> ) } }
這次我們去掉了shouldComponentUpdate,繼承基類我們改成了PureComponent,輸出結果:
Test state change.
很好,達到了我們想要的效果,而且程式碼量也減小了,但是真的可以做到完全的防止元件無畏的render嗎?讓我們來看看PureComponent的實現原理
最重要的程式碼在下面的檔案裡面,當然這個是React 16.2.0版本的引用
/node_modules/fbjs/libs/shallowEqual
大致的比較步驟是:
1.比較兩個Obj物件是否完全相等用===判斷
2.判斷兩個Obj的鍵數量是否一致
3.判斷具體的每個值是否一致
不過你們發現沒有,他只是比對了第一層次的結構,如果對於再多層級的結構的話就會有很大的問題
來讓我們修改原始碼再來嘗試:
class A extends PureComponent { constructor(props){ super(props); } static propTypes = { value:PropTypes.number, obj:PropTypes.object }; static defaultProps = { value:0, obj:{} }; componentDidUpdate() { console.log("A componentDidUpdate"); } render (){ return ( <div /> ) } } class Test extends Component { constructor(props) { super(props); this.state={ value:0, obj:{a:{b:123}} }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(()=>{ console.log("Test state change."); let {obj,value} = this.state; //這裡修改了裡面a.b的值 obj.a.b=456; this.setState({ value:value+1, obj:obj }) },100); } render() { let { state } = this; let { value, obj } = state; return ( <A obj={obj} /> ) } }
輸出結果:
Test state change.
這裡不可思議吧!這也是很多人對引用型別理解理解不深入所造成的,對於引用型別來說可能出現引用變了但是值沒有變,值變了但是引用沒有變,當然這裡就暫時不去討論js的資料可變性問題,要不然又是一大堆,大家可自行百度這些
那麼怎麼樣做才能真正的處理這樣的問題呢?我先增加一個基類:
import React ,{Component} from `react`; import {is} from `immutable`; class BaseComponent extends Component { constructor(props, context, updater) { super(props, context, updater); } shouldComponentUpdate(nextProps, nextState) { const thisProps = this.props || {}; const thisState = this.state || {}; nextState = nextState || {}; nextProps = nextProps || {}; if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) { return true; } for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } } for (const key in nextState) { if (!is(thisState[key], nextState[key])) { return true; } } return false; } } export default BaseComponent
大家可能看到了一個新的東西Immutable,不瞭解的可以自行百度或者 Immutable 常用API簡介 , Immutable 詳解
我們來改寫之前的程式碼:
import React, {PureComponent,Component} from "react" import PropTypes from "prop-types" import Immutable from "immutable" import BaseComponent from "./BaseComponent" class A extends BaseComponent { constructor(props){ super(props); } static propTypes = { value:PropTypes.number, obj:PropTypes.object }; static defaultProps = { value:0, obj:{} }; componentDidUpdate() { console.log("A componentDidUpdate"); } render (){ return ( <div /> ) } } class Test extends Component { constructor(props) { super(props); this.state={ value:0, obj:Immutable.fromJS({a:{b:123}}) }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(()=>{ console.log("Test state change."); let {obj,value} = this.state; //注意,寫法不一樣了 obj = obj.setIn(["a","b"],456); this.setState({ value:value+1, obj:obj }) },100); } render() { let { state } = this; let { value, obj } = state; return ( <A obj={obj} /> ) } }
執行結果:
Test state change. A componentDidUpdate
這樣也達到了我們想要的效果
當然,還有一種比較粗暴的辦法就是直接把obj換成一個新的物件也同樣可以達到跟新的效果,但是可控性不大,而且操作不當的話也會導致過多的render,所以還是推薦使用immutable對結構層級比較深的props進行管理
上面的一大堆主要是講述了對基本型別以及Object(Array 其實也是Object,這裡就不單獨寫示例了)型別傳值的優化,下面我們來講述關於function的傳值
function其實也是Object,但是純的function比較特麼,他沒有鍵值對,無法通過上面提供的方法去比對兩個function是否一致,只有通過引用去比較,所以改不改引用成為了關鍵
改了下程式碼:
import React, {PureComponent,Component} from "react" import PropTypes from "prop-types" import Immutable from "immutable" import BaseComponent from "./BaseComponent" class A extends BaseComponent { constructor(props){ super(props); } static propTypes = { value:PropTypes.number, obj:PropTypes.object, onClick:PropTypes.func }; static defaultProps = { value:0, obj:{} }; componentDidUpdate() { console.log("A componentDidUpdate"); } render (){ let { onClick } = this.props; return ( <div onClick={onClick} > 你來點選試試!!! </div> ) } } class Test extends Component { constructor(props) { super(props); this.state={ value:0, }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(()=>{ console.log("Test state change."); let {value} = this.state; this.setState({ value:value+1, }) },100); } onClick(){ alert("你點選了一下!") } render() { let { state } = this; let { value, obj } = state; return ( <A onClick={()=>this.onClick()} /> ) } }
執行結果:
Test state change. A componentDidUpdate
我們setState以後控制元件A也跟著更新了,而且還用了我們上面所用到的BaseComponent,難道是BaseComponent有問題?其實並不是,看Test元件裡面A的onClick的賦值,這是一個匿名函式,這就意味著其實每次傳入的值都是一個新的引用,必然會導致A的更新,我們這樣幹:
class Test extends Component { constructor(props) { super(props); this.state={ value:0, }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(()=>{ console.log("Test state change."); let {value} = this.state; this.setState({ value:value+1, }) },100); } onClick=()=>{ alert("你點選了一下!") }; render() { let { state } = this; let { value } = state; return ( <A onClick={this.onClick} /> ) } }
輸出結果:
Test state change.
嗯,達到我們想要的效果了,完美!
不過我還是發現有個問題,如果我在事件或者回撥中需要傳值就痛苦了,所以在寫每個元件的時候,如果有事件呼叫或者回撥的話最好定義一個接收任何型別的屬性,最終的程式碼類似下面這樣
import React, {PureComponent, Component} from "react" import PropTypes from "prop-types" import Immutable from "immutable" import BaseComponent from "./BaseComponent" class A extends BaseComponent { constructor(props) { super(props); } static propTypes = { value: PropTypes.number, obj: PropTypes.object, onClick: PropTypes.func, //增加data傳值,接收任何型別的引數 data: PropTypes.any }; static defaultProps = { value: 0, obj: {}, data: "" }; componentDidUpdate() { console.log("A componentDidUpdate"); } //這裡也進行了一些修改 onClick = () => { let { onClick, data } = this.props; onClick && onClick(data); }; render() { return ( <div onClick={this.onClick}> 你來點選試試!!! </div> ) } } class Test extends Component { constructor(props) { super(props); this.state = { value: 0, }; } static propTypes = {}; static defaultProps = {}; componentDidMount() { setTimeout(() => { console.log("Test state change."); let {value} = this.state; this.setState({ value: value + 1, }) }, 100); } onClick = () => { alert("你點選了一下!") }; render() { let { state } = this; let { value } = state; return ( <A onClick={this.onClick} data={{message: "任何我想傳的東西"}} /> ) } }
總結一下:
1.編寫React元件的時候使用自定義元件基類作為其他元件的繼承類
2.使用Immutable管理複雜的引用型別狀態
3.傳入function型別的時候要傳帶引用的,並且注意預留data引數用於返回其他資料
如果大家有什麼意見或者建議都可以在評論裡面提哦