作者:ManfredHu
鏈接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinciple
聲明:版權所有,轉載請保留本段信息,否則請不要轉載
React
React的優點有很多,現在很多應用都接入React這個框架。
在我看來,有下列優點:
- Facebook團隊研發並維護——有團隊維護更新且有質量保證
- 在MVVM結構下只起View的作用——簡單接入,不需要花費大量人力重構代碼
- 組件化形式構建Web應用——復用性強,提高開發效率
- 用Virtual DOM減少對DOM的頻繁操作提高頁面性能——批量操作減少重排(reflows)和重繪(repaints)次數——性能對比舊的方式有提高
React對重排和重繪的提高
雅虎性能優化比較重要的點,老司機自行忽略。
如下圖,HTML被瀏覽器解析為DOM樹,CSS代碼加載進來解析為樣式結構體,兩者關聯組成渲染樹,之后瀏覽器把渲染樹繪制出來就是我們看到的網頁了。這里如果我們對DOM樹或者樣式結構體做一些操作,如刪除某個節點,樣式改為隱藏(display:none)等等,會觸發重排進而導致重繪。
觸發重排的條件
- DOM元素的數量屬性變化
- DOM樹的結構變化——節點的增減、移動
- 某些布局屬性的讀取和設置觸發重排——offsetTop/offsetWidth/scrollTop等等
導致子級、后續兄弟元素、父節點因重新計算布局而重排
觸發重繪的條件
- 簡單樣式屬性的變化——顏色、背景色等
- 重排導致的重繪
而React維護了一個Virtual DOM將短時間的操作合並起來一起同步到DOM,所以這也是它對整個前端領域提出的最重要的改變。
為什么引入Reflux?
上面說了React在MVVM結構下只起View的作用,那么除了View,MVVM下還有Model,ViewModel。
而純粹的View,會讓整個邏輯耦合在一層下,數據也需要層層傳遞,不方便控制和復用。
故業內也有一堆的分層框架——如最早的flux,現在部門在用的Reflux,以及Redux。
對比Redux,Reflux更容易理解和上手——這也是現狀,學習成本越低,接入現有業務就越容易。
Reflux
reflux的架構非常簡單,就是三部分
- Action 理解為一個命令或者動作,通過它來向組件發出”指令”
- Store 為ViewModel部分,組件的一些狀態屬性會存儲在這里
- View Component 為組件模板
所以Reflux只是讓我們,更好的去操作組件,通過一個Action命令,叫組件去干嘛,組件自己通過寫好的代碼,對命令做出反應(變化為不同的state狀態)。
React+Reflux起到的作用
現在你已經有了兩個小工具了,寫一個組件,通過Action調用組件就可以了。
寫到這里,你應該能體會到,所有的引入就是為了讓代碼寫起來更有效率,更易用,復用性更強。
Pure Component
純凈的組件:在給定相同props和state的情況下會渲染出同樣結果
其優點有這么幾點:
- 我們寫的組件都應該是只依賴props和state的,而不應該依賴其他全局變量或參數
- 純凈的組件方便復用、測試和維護
組件生命周期
React組件有兩部分
第一部分是初始化的生命周期:
- getDefaultProps
- geInitialState
- componentWillMount
- render
- componentDidMount
第二部分是被action觸發,需要更新:
- shouldComponentUpdate
- componentWillUpdate
- render
- conponentDidUpdate
shouldComponentUpdate
shouldComponentUpdate這個方法可以說是一個預留的插入接口。
在上面更新的時候,第一步就是調用的這個方法判斷組件是否該被重新渲染。
shouldComponentUpdate是在React組件更新的生命周期中,用於判斷組件是否需要重新渲染的一個接口,它有兩個返回值:
- 返回true,則進入React的Virtual DOM比較過程
- 返回false,則跳過Virtual DOM比較與渲染等過程
如上圖,這是一棵React Virtual DOM的樹。
- C1在
ShouldComponentUpdate
返回了true,即默認值,代表需要更新,進入Virtual DOM Diff
過程,返回false,不相同,需要更新 - C2在
ShouldComponentUpdate
返回了false,不再更新,C4,C5因為被父節點在ShouldComponentUpdate
中返回了false,所以不再更新 - C3在
ShouldComponentUpdate
返回了true進入Virtual DOM Diff
過程,比對結果為false,新舊不一樣,需要更新 - 輪到C6,
ShouldComponentUpdate
返回了true,進入Virtual DOM Diff
的過程,返回了false,即新舊兩個節點不相同,所以這個節點需要更新 - C7在
ShouldComponentUpdate
返回了false,即不需要更新,節點不變 - C8在
ShouldComponentUpdate
返回了true,進入Virtual DOM Diff
比對過程,結果為true,新舊相等,不更新
大概就是這么一個過程,在這里,Diff算法其實還是比較復雜的,比較好的做法是我們來寫入ShouldComponentUpdate來自己控制組件的更新,而不是依賴React幫我們做比較。
進入正文
前面講了那么多,相信懂React的都懂了,就不再詳細講了,Diff算法有興趣的可以自己去翻源碼,網上也有一堆模擬實現的例子。
接下來介紹一個探索reflux&react渲染優化的例子。
這里試圖,模擬一個比較現實的例子,拋開很多業務代碼,讓問題變得直接。
首先例子有三個組件,兩個按鈕,5個數字,還有一個重復打印文本的大組件。
- 1basicDemo 是沒有優化的例子,每50ms會發出action更改store數據觸發渲染
- 2perfDemo 使用addons插件Perf分析頁面性能的例子
- 3pureRenderMixinDemo 使用addons插件pureRenderMixin優化頁面性能的例子
- 4updateDemo 使用了addons插件update優化頁面性能的例子
- 5immutableDemo 使用了
Immutable.js
優化頁面性能的例子
源代碼請點擊這里
說明
- gulpfile.js為gulp構建代碼,會將
tpl.js
的JSX代碼翻譯為js代碼,需要的可以自己修改,每次轉化模板需要gulp
運行一下 - modulejs模塊加載器和myView單頁SPA框架為騰訊通訊與彩票業務部前端團隊這邊的基本框架,具體的請戳這里查看
- 需要關注的文件
- index.html 頁面入口,規定了執行的模塊
- app.js 應用程序入口
- todoAction.js (reflux架構下,demo的action)
- todoStore.js (reflux架構下,demo的store)
- tpl.js 組件的jsx文件
簡單用法
cd ./xxx/
(這里的xxx為上面對應的 ……./4updateDemo/ 目錄)http-server -p 8888
端口可以自定義,http-server模塊已在node_module
目錄下,擔心版本依賴問題,已上傳node_module
目錄,直接打開就可以了- 打開瀏覽器便可瀏覽,詳情請看控制台
1.basicDemo
1basicDemo目錄是一個最原始的目錄,這里你可以看到我們哪里出現了問題。
cd ./example
打開這個沒優化過的例子的目錄
http-server -p xxxx
這里端口隨意,不沖突就好
瀏覽器訪問並打開控制台,會看到
5 tpl.js:32 createNum組件被更新了
tpl.js:10 TextComponent被更新了
2 tpl.js:57 createBtn組件被更新了
初始化createNum組件被渲染了5次,因為有5個,createBtn組件被渲染了兩次,因為有點擊開始和點擊結束兩個按鈕。通過不同的傳參而改變形態。
點擊開始會觸發action
,讓store的數據每次+1,點擊結束會清除定時器
點擊開始可以看到控制台的數據每次都會刷新整個界面的所有組件,特別是有一個大組件TextComponent
,是重復5000次文本的,每次重新渲染就有很多的損耗。這就是我們要優化的地方——減少某些關鍵部分的重新渲染的次數,減少無用對比的消耗
這里你可以打開Chrome控制台的Timeline來看一下,點擊開始,打開Timeline面板,每1S左右會有一個腳本執行的高峰期。
我們知道特別是在移動端,CPU和內存的資源顯得尤為稀缺(大概只能占用正常CPU和內存的10%,微信手Q等可能會因為友商系統對應用程序的優先級設計使這個限制略有提高——我說的就是小米哈哈哈),所以這樣說來,性能這一塊在移動手機web顯得非常非常重要。
2.Perl
Perl是react-addons帶來的性能分析工具,這里的perfDemo是結合Chrome插件的例子。
要向全局暴露一個window.Perl
變量,然后就可以愉快的配合Chrome插件使用了
- React-addons插件版本的Perf插件提供原生的API——用在首次渲染部分
- Chrome插件——用在有交互的部分
- console tool——需要查看對比新舊值的情況下
這里的wasted time就是在做屬性沒變化的重復渲染的過程,可以優化。
用法與Chrome開發工具的TimeLine用法類似,點擊start開始記錄,后點擊stop結束
3.PureRenderMixin
一個簡單的通用優化工具,通過淺對比(shallowCompare)方法對比新舊兩個組件的狀態,達到減少重復渲染的目的。
注意這里組件的store必須無關聯,原因是shallowCompare的時候,比較的是組件關聯的store的數據,而例子里面store是一個,其他組件num的變化也會引起這里TextComponent組件的更新
這里將store與頂級組件APP關聯起來,然后在子孫組件下自定采用props傳遞的方式處理(傳遞基本類型的數據),這樣就可以讓pureRenderMixin的通用化了,唯一的缺點是,傳遞props要控制,只把組件需要的屬性傳遞下去,這里會比較麻煩,但是這樣又是性能較高又比較好理解的處理方式(相對其他要拷貝屬性的方式)
*store下,option里面的對象,受pureRenderMixin的限制,不可以出現引用類型
PureRenderMixin其實是封裝了更底層的shallowCompare接口的
簡單用法如下:
var PureRenderMixin = require('react').addons.PureRenderMixin;
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});
就加了一個mixins,看起來簡單優雅有木有。可以在眾多組件里面copy通用啊有木有
那這里干了什么?
React.addons = {
CSSTransitionGroup: ReactCSSTransitionGroup,
LinkedStateMixin: LinkedStateMixin,
PureRenderMixin: ReactComponentWithPureRenderMixin, //看這里
var ReactComponentWithPureRenderMixin = {
//幫你寫了一個shouldComponentUpdate方法
shouldComponentUpdate: function (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
};
function shallowCompare(instance, nextProps, nextState) {
//分別比較props和state屬性是否相等
return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
}
function shallowEqual(objA, objB) {
if (objA === objB) { //store嵌套層級太深這里就會返回true,引用類型內存指向同一空間
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
var bHasOwnProperty = hasOwnProperty.bind(objB);
for (var i = 0; i < keysA.length; i++) {
if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
return false;
}
}
return true;
}
所以PureRenderMixin這個插件,只能比較state和props為基本類型的部分。
如果有更加深層次的store數據嵌套,就要借助於update插件或者Immutablejs來深拷貝store的數據另存一份了。
4.用update優化(也稱Immutable Helper)
update是addons里面的一個方法,旨在對拷貝對象復雜的過程來做一些語法上的優化,具體可以看react官方文檔
//extend復制對象屬性的時候
var newData = extend(myData, {
x: extend(myData.x, {
y: extend(myData.x.y, {z: 7}),
}),
a: extend(myData.a, {b: myData.a.b.concat(9)})
});
//用update的時候,提供了一些語法糖讓你不用寫那么多
var update = require('react-addons-update');
var newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
cd ./updateDemo
打開這個用addons.update優化過的例子的目錄
http-server -p xxxx
這里端口隨意,不沖突就好
這個例子與上面一個例子唯一的不同是這里用了addons.update來進行store數據的復制,具體的可以看todoStore和tpl這兩個模塊的代碼,其他基本無修改
這里update是參考了MongoDB’s query的部分語法,具體的可以看這里,類比數組方法,返回一個新的實例。
- {$push: array} 類似數組的push方法
- {$unshift: array} 類似數組的unshift方法
- {$splice: array of arrays} 類似數組的splice方法
- {$set: any} 整個替換目標
- {$merge: object} 合並目標和object的 keys.
- {$apply: function} 傳遞當前的值給 function 並用返回值更新它
但是由Timeline的觀察來看,復制對象屬性的性能遠比刷新一個大組件的性能高。
5.Immutablejs
Immutable.js是Facebook為解決數據持久化而獨立出來的一個庫,傳統的,比如我們有
var a = {b:1};
function test(obj){
obj.b = 10;
return obj;
}
test(a); //10
函數對對象的操作,你不會知道這個函數對對象進行了什么操作。也就是說是封閉的。
而Immutable每次對對象的操作都會返回一個新對象
Immutable.js提供了7種不可變的數據類型:List Map Stack OrderedMap Set OrderedSet Record
,對Immutable對象的操作均會返回新的對象,例如:
var obj = {count: 1};
var map = Immutable.fromJS(obj);
var map2 = map.set('count', 2);
console.log(map.get('count')); // 1
console.log(map2.get('count')); // 2
引入Immutable.js,需要對現有的業務代碼進行改動,通常是對tpl和store兩部分進行操作,初始化數據的時候生成一個Immutable的數據類型,之后每次get,set操作都會返回一個共享的新的對象。
50ms渲染一次,重復渲染200次的截圖,引入了immutable用了其set方法:
50ms渲染一次,重復渲染200次的截圖,引入了immutable用了其update方法:
6.seamless-immutable && Observejs
一個是immutable的閹割版,一個是AlloyTeam推的。
兩者都是通過Object.defineProperty(IE9+)對set和get操作進行處理,優點是文件比較小。
7.寫在最后
自己設想,組件化運用到極致,應該是像微信weui那樣
- 有一套非常適合接入,復用性非常強的組件庫。拿來就用,不需要再次開發
- 應該兼顧起上面說的減少重復渲染的部分
- 開發友好
這里也思考一些可能做到的變化:
- 將一個組件的action/store/JSX/樣式代碼Style 寫在一個文件里,這樣方便修改和調用,封閉組件內部實現細節,對外只暴露action操作和store的一些get方法,這樣可以修改或者是獲取到組件的某些現在時刻的屬性(也有同學是直接封裝為一個對象,通過對象暴露其store,action)
- 組件共享或依賴的數據,應在公共父級的store或獨立成一個單獨的部分,然后采用props傳遞的形式或從獨立的store里面取數據
License