React爬坑祕籍(一)——提高渲染性能

React爬坑祕籍(一)——提高渲染性能

##前言

來到騰訊實習後,有幸八月份開始了騰訊辦公助手PC端的開發。由於辦公助手主推的是移動端,因此導師也是大膽的讓咱們實習生來技術選型並開發,他來作code review。以前也學習過React,固然也是很是合適這一次的開發。javascript

我會梳理這一個月來,本身對架構的思考過程和踩過的坑。固然這一切都不必定是最佳的,因此但願能有更多的建議和討論。前端

例子所需庫:Webpack、React、Immutable。其中Webpack用於前端構建,若是不清楚的同窗能夠看這裏:webpack前端構建體驗java

##出現場景

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

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

在這裏我但願你們知道有一點就是,當父組件的狀態state發生變化時,傳入state的子組件都會進行從新渲染。git

下面咱們來模擬一下這種狀況,一塊兒來看看。github

/** * 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個子組件從新渲染的狀況。因此表現出來的狀況就是,我點一下,等個一兩秒那個框才真正被勾上。我相信用戶在這一秒內確定已經關掉頁面了。web

若是對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進行無刷新的更新,很是的酷炫!我在配置它的過程當中也摸了一些坑,因此但願能幫助你們跳過這個坑。相信若是能好好使用它,將會大大提高你們的開發效率。

相關文章
相關標籤/搜索