很多人在寫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參數用於返回其他數據
如果大家有什么意見或者建議都可以在評論里面提哦
