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