React爬坑秘籍(一)——提升渲染性能


React爬坑秘籍(一)——提升渲染性能

前言


來到騰訊實習后,有幸八月份開始了騰訊辦公助手PC端的開發。因為辦公助手主推的是移動端,所以導師也是大膽的讓我們實習生來技術選型並開發,他來做code review。之前也學習過React,當然也是非常合適這一次的開發。

我會梳理這一個月來,自己對架構的思考過程和踩過的坑。當然這一切都不一定是最佳的,所以希望能有更多的建議和討論。

例子所需庫:Webpack、React、Immutable。其中Webpack用於前端構建,如果不清楚的同學可以看這里:webpack前端構建體驗

出現場景


一般來說,React作為一個高效的UI Library,如果合理使用是很難出現性能問題的。它內部提供了虛擬DOM搭配上Diff算法,和子組件必要的key屬性,都是非常優秀的優化了絕大部分的性能。

但是我們來模擬一個場景,在一個數組里有10000個對象,我們把這個數組的數據渲染出來后,其中一個屬性用於控制頁面狀態。

在這里我希望大家知道有一點就是,當父組件的狀態state發生變化時,傳入state的子組件都會進行重新渲染。

下面我們來模擬一下這種情況,一起來看看。

/**
 * Created by YikaJ on 15/9/17.
 */
'use strict';
var React = require("react");

var App = React.createClass({

    getInitialState(){
        return {
            list: this.props.dataArr
        }
    },

    // 對數據的狀態進行變更
    toggleChecked(event){
        let checked = event.target.checked;
        let index = event.target.getAttribute("data-index");
        let list = this.state.list;
        list[index].checked = checked;

        this.setState({list});
    },

    render(){
        // 將數組的數據渲染出來
        return (
            <ul>
                {this.state.list.map((data, index)=>{
                    return (
                        <ListItem data={data}
                            index={index} key={data.name}
                            toggleChecked={this.toggleChecked}
                        />
                    )
                })}
            </ul>
        )
    }
});

// 代表每一個子組件
var ListItem = React.createClass({
    render(){
        let data = this.props.data;
        let index = this.props.index;
        
        // checkbox選擇框是一個受限組件,用數據來決定它是否選中
        return (
            <li>
                <input type="checkbox" data-index={index} checked={data.checked} onChange={this.props.toggleChecked}/>
                <span>{data.name}</span>
            </li>
        )
    }
});

// 構造一個2000個數據的數組
let dataArr = [];
for(let i = 0; i < 2000; i++){
    let checked = Math.random() < 0.5;
    dataArr.push({
        name: i,
        checked
    });
}

React.render(<App dataArr={dataArr}/>, document.body);

這個就是我們的有性能問題的組件。當我們去點擊選框時,因為父組件的state傳到了子組件的props里,我們就會遇到10000個子組件重新渲染的情況。所以表現出來的情況就是,我點一下,等個一兩秒那個框才真正被勾上。我相信用戶在這一秒內肯定已經關掉頁面了。

如果對React很熟悉的人,肯定知道一個生命周期的Hook,就是shouldComponentUpdate(nextProps, nextState)。這個API就是用來決定該組件是否重新Render。所以我們肯定很開心的說,只要屬性的checked值不變,就不渲染唄!

// return true時,進行渲染;false時,不渲染
shouldComponentUpdate(nextProps, nextState){
    if(this.props.data.checked !== nextProps.data.checked){
        return true;
    }
    return false;
}

就這么簡單么~我保存編譯JSX后,迫不及待的刷新瀏覽器看一看了。一按
嗯,呵呵,組件都不會渲染了...那說明this.props.datanextProps.data的數據是一致的,這怎么可能?!我明明是通過父組件的函數修改了數組然后重新setState的呀!

修改數組......嗯,當時我就意識到這肯定又和引用類型有關。我相信大家既然能看到這里,相信基礎都是有的,就是數據的基本類型和引用類型的差別,但是我還是樂意再用代碼展示一次。

// 基本類型,number boolean string undefined null
var a = 10;
var b = a;
a = 12;
console.log(b) // => 10

// 引用類型,Object Function Array
var a = [{checked: false}, {checked: true}];
var b = a;
a[0].checked = true;
console.log(b) // => [{checked: true}, {checked: true}]

我們明顯可以看到它們的差別,我們這里着重注意一下引用類型。因為變量不再直接存值,而是變成了存指針。所以我們的每一次都同一個指針所指內存進行修改時,都會影響到擁有該指針的變量。這里當然a和b都是指的同一個對象,所以他們修改的數據也同樣是同步的。

對,我們的this.props.datanextProps.data指的是同一個東西,所以任何修改都不會讓它們區分開。那這樣我們是不是就要開始考慮如何進行深拷貝?

深拷貝表示只是路過打個醬油


我們在開發過程中,既可以享受到使用引用類型的特點帶來的便利,但是同時也會忍受到非常多稀奇古怪的問題,總而言之,弊大於利。

思路其實就是將一個引用類型通過遞歸的方式,逐層向下取最小的基本類型,然后拼裝成一樣的引用類型。一看就是耗性能的主啊!如果真有這個深拷貝需求的同學,這里推薦的是lodash庫的_.cloneDeep方法,它是據我所知最完善的深拷貝方法。

當然如果你的引用類型並不復雜,例如沒有函數或正則,只包含扁平化的數據時,我這里推薦一個奇淫巧計。

var newData = JSON.parse(JSON.stringify(data));

其實在我們這次這個案例里,就非常適合這個JSON序列化后再反序列化的方法,因為我們的數據其實也就是扁平化的。我們把它放到函數內看一下效果。

toggleChecked(event){
    let checked = event.target.checked;
    let index = event.target.getAttribute("data-index");
    let list = JSON.parse(JSON.stringify(this.state.list));
    list[index].checked = checked;

    this.setState({list});
},

這個世界瞬間清爽多了。但是我們知道,在真正的開發過程中,不一定可以用這種奇淫巧計的,那我們除了實在沒辦法耗性能的deepClone,我們還能怎么辦?怎么辦!?

Immutable Data


Facebook自家有一個專門處理不可變數據的庫,immutable-js。我們知道,React其實是非常接近函數式編程的思想的,我們可以用下面這個式子來表示React的渲染。

UI = fRender(state, props);

Immutable Data(不可變數據)的思想就是,不存在指向同一地址的變量,所有對Immutable Data的改變,最終都會返回一份新復制的數據,各自的數據並不會互相影響。在構建大型應用時,應該非常注意這樣的數據獨立性,不然你連數據在哪兒被改了你或許都不知道。那說了這么多它的概念,實際使用的時候是怎么樣的?

// 這段代碼可以直接在Immutable的文檔頁面的控制台執行
var arr = Immutable.fromJS([1]);
var arr1 = arr.push(2);
console.log(arr.toJS(), arr1.toJS()); // => [1], [1,2]  

我們執行后,確實原有的數據已經不可變了,又新生成了一個新的不可變數據,其實這里有個非常有趣的應用場景就是撤銷。不用再擔心引用類型數據的變化,因為一切數據都被你把控了。

我相信有人肯定好奇說,我每一次操作數據時都deepClone一下,也可以達到這種效果呀,這里的實現有什么不一樣嗎?deepClone是通過遞歸對象進行數據的拷貝,而Immutable數據的實現則是僅僅拷貝父節點,而其他不受影響的數據節點都是共享的用同一份數據,以大大提升性能。我們需要做的僅僅是將原生的數據轉化成Immutable數據。

我知道僅僅通過語言是很難生動表現出來的,所以找到幾幅圖來進行解釋。


我們需要修改某個節點的數據,這個節點用黃色標了出來。

按照我們剛才所說的,僅對父節點進行一次數據的拷貝,我們把全新的數據拉出來,拷貝的是綠色的節點。

而其他的節點數據其實並不受影響,所以我們可以直接使用他們的內存地址,共享一份數據。共享的數據,我們用橙色標出。

最后我們以最優的性能得到了一份全新的數據。


當我們在shouldComponentUpdate里判斷是否更新時,變化的數據是新的引用,而不變的數據是原來的引用,這樣我們就可以非常輕松的判斷新舊數據的差異,從而大大提升性能。那我們知道了這個Immutable可以很好的解決我們的痛點之后,我們該如何使用到我們的實際項目中呢?其實很簡單的,就是數據初始化時,就讓它變成Immutable數據,然后之后對數據的操作就可以參照一下文檔了,這里我直接重寫了demo,其實也就是把取值和賦值做個改變,我會用注釋標識出來。

/**
 * Created by YikaJ on 15/9/17.
 */
'use strict';
var React = require('react');
var Immutable = require('immutable');

var App = React.createClass({

    getInitialState(){
        return {
            // 這里將傳入的數據轉化成Immutable數據
            list: Immutable.fromJS(this.props.dataArr)
        }
    },

    // 對數據的狀態進行變更
    toggleChecked(event){
        let checked = event.target.checked;
        let index = event.target.getAttribute("data-index");
        
        // 這里不再是直接修改對象的checked的值了,而是通過setIn,從而獲得一個新的list數據
        let list = this.state.list.setIn([index, "checked"], checked);

        this.setState({list});
    },

    render(){
        return (
            <ul>
                {this.state.list.map((data, index)=>{
                    return (
                        <ListItem data={data}
                            index={index} key={index}
                            toggleChecked={this.toggleChecked}
                        />
                    )
                })}
            </ul>
        )
    }
});

// 代表每一個子組件
var ListItem = React.createClass({

    shouldComponentUpdate(nextProps){
        // 這里直接對傳入的data進行檢測,因為只需要檢測它們的引用是否一致即可,所以並不影響性能。
        return this.props.data !== nextProps.data;
    },

    render(){
        let data = this.props.data;
        let index = this.props.index;

        // 取值也不再是直接.出來,而是通過get或者getIn
        return (
            <li>
                <input type="checkbox" data-index={index} checked={data.get("checked")} onChange={this.props.toggleChecked}/>
                <span>{data.get("name")}</span>
            </li>
        )
    }
});

// 構造一個2000個數據的數組
let dataArr = [];
for(let i = 0; i < 2000; i++){
    let checked = Math.random() < 0.5;
    dataArr.push({
        name: i,
        checked
    });
}

React.render(<App dataArr={dataArr}/>, document.body);

就這樣,我們非常優雅的解決了引用類型帶來的問題。其實Immutable的功能並不只這些。它內部提供了非常多種的數據結構以供使用,例如和ES6一致的Set,這種特殊的數組不會存有相同的值。相信利用好不同的數據結構,會非常有利於你構建復雜應用。

PureRenderMixin表示也要來打個醬油


這里插多個React.addons內添加的東西,在我一開始探索這些性能相關問題的時候,我就注意到了這個東西。它會自行為該組件增添shouldComponentUpdate,對現有的子組件的state和props進行判斷。但是它只支持基本類型的淺度比較,所以實際開發時並不能直接拿來使用。但是!我們一旦使用了Immutable數據后,比較是否是同一指針這樣的事情,自然就是淺比較,所以換句話而言,我們可以使用PureRenderMixin配合上Immutable,非常優雅的實現性能提升,而且我們也不用再手動去shouldComponentUpdate進行判斷。

var React = require("react/addons");

var ListItem = React.createClass({
    mixins: [React.addons.PureRenderMixin],
    
    // .....以下代碼省略
});

總結


我相信這次提供的方法,已經可以非常優雅的解決絕大部分的性能問題了。但如果還不行,那么你可能要對你的業務邏輯代碼進行優化了。下一篇,我將會介紹一下React-hot-loader這一開發神器,它可以利用webpack的模塊熱插拔的特性,實時對瀏覽器的js進行無刷新的更新,非常的酷炫!我在配置它的過程中也摸了一些坑,所以希望能幫助大家跳過這個坑。相信如果能好好使用它,將會大大提升大家的開發效率。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM