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.data
和nextProps.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.data
和nextProps.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進行無刷新的更新,非常的酷炫!我在配置它的過程中也摸了一些坑,所以希望能幫助大家跳過這個坑。相信如果能好好使用它,將會大大提升大家的開發效率。