編寫高效能React元件-傳值篇

你猜不猜發表於2018-08-31

很多人在寫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引數用於返回其他資料

 

如果大家有什麼意見或者建議都可以在評論裡面提哦

 

相關文章