標籤:nts 自己的 connect 推薦 預設 適合 答案 state lstat
大家都知道,react的一個痛點就是非父子關係的組件之間的通訊,其官方文檔對此也並不避諱:
For communication between two components that don‘t have a parent-child relationship, you can set up your own global event system. Subscribe to events in componentDidMount(), unsubscribe in componentWillUnmount(), and call setState() when you receive an event.
而redux就可以視為其中的“global event system”,使用redux可以使得我們的react應用有更加清晰的架構。
本文我們來探討,基於react和redux架構的前端應用,如何進行渲染效能最佳化。對於小型react前端應用,最好的最佳化就是不最佳化
因為React本身就是通過比較虛擬DOM的差異,從而對真實DOM進行最小化操作,小型React應用的虛擬DOM結構簡單,虛擬DOM比較的耗時可以忽略不計。而對於複雜的前端項目,我們所指的渲染效能最佳化,實際上是指,在不需要更新DOM時,如何避免虛擬DOM的比較
。
1. react組件的生命週期
工欲善其事,必先利其器。理解react的組件的生命週期是最佳化其渲染效能的必備前提。我們可以將react組件的生命週期分為3個大迴圈:掛載到DOM、更新DOM、從DOM中卸載。React對三個大迴圈中每一步都暴露出鉤子函數,使得我們可以細粒度地控制組件的生命週期。
(1)掛載到DOM
組件首次插入到DOM時,會經曆從屬性和狀態初始化到DOM渲染等基本流程,可以通過描述:
必須注意的是,掛載到DOM流程在組件的整個生命週期只有一次,也就是組件第一次插入DOM文檔流時。在掛載到DOM流程中的每一步也有相應的限制:
getDefaultProps()和getInitialState()中不能擷取和設定組件的state。render()方法中不能設定組件的state。
(2)更新DOM
組件掛載到DOM後,一旦其props和state有更新,就會進入更新DOM流程。同樣我們也可以通過一張圖清晰的描述該流程的各個步驟:
componentWillReceiveProps()提供了該流程中更新state的最後時機,後續的其他函數都不能再更新群組件的state了。我們尤其需要注意的是shouldComponentUpdate函數,它的結果直接影響該組件是不是需要進行虛擬DOM比較
,我們對組件渲染效能最佳化的基本思路就是:在非必要的時候將shouldComponentUpdate傳回值設定為false,從而終止更新DOM流程中的後續步驟。
(3)從DOM中卸載
從DOM中卸載的流程比較簡單,React只暴漏出componentWillUnmount
,該函數使得我們可以在DOM卸載的最後時機對其進行幹預。
2. react組件渲染效能監控
在進行效能最佳化前,我們先來瞭解如何對React組件渲染效能進行監控。React官方提供了Performance Tools,其使用起來也很簡單,通過Perf.start啟動一次效能分析,並通過Perf.stop結束一次效能分析。
import Perf from ‘react-addons-perf‘Perf.start();....your react codePerf.stop();
調用Perf.stop後,我們就可以通過Perf提供的API來擷取本次效能分析的資料指標。其中最有用的API是Perf.printWasted()
,其結果給出你在哪些組件上進行了無意義的(沒有引起真實DOM的改變)虛擬DOM比較,比如如下結果表明我們在TodoItem組件上浪費了4ms進行無意義的虛擬DOM比較,我們可以從這裡入手,進行效能最佳化。
而Perf.printInclusive()
的結果則給出渲染各個組件的總體時間,通過它的結果我們可以找出哪個組件是頁面渲染的效能瓶頸。
和Perf.printInclusive()
相似的API還有Perf.printExclusive()
,只是其結果是組件渲染的專屬時間,即不包括花費於載入組件的時間: 處理 props, getInitialState, 調用 componentWillMount 及 componentDidMount, 等等。
3. 效能最佳化基本原理
使用上一小節的效能分析工具,我們可以輕易的定位出哪些組件是頁面的效能瓶頸、哪些組件進行了無意義的虛擬DOM比較,本小節我們能探討如何對基於react和redux架構的前端應用進行效能最佳化。
3.1 常規React組件效能最佳化
通過上文的React更新DOM流程,我們知道React提供了shouldComponentUpdate
函數,它的結果直接影響組件是不是需要進行虛擬DOM比較以及後續的真實DOM渲染。而shouldComponentUpdate
函數的預設傳回值為true,這暗示著React總是會進行虛擬DOM比較,無論真實DOM是否需要重新渲染。我們可以通過根據自己的業務特性,重載shouldComponentUpdate
,只在確認真實DOM需要改變時,再返回true。一般的做法是比較組件的props和state是否真的發生變化,如果發生變化則返回true,否則返回false。
shouldComponentUpdate: function (nextProps, nextState) { return !isDeepEqual(this.props,nextProps) || !isDeepEqual(this.state,nextState); }
進行深度比較(isDeepEqual)來確定props和state是否發生變化是最常見的做法,其是否有效能問題呢?如果一個容器型組件有很多的子節點,而子節點又有其他子節點,對這種複雜的嵌套對象進行深度比較(isDeepEqual)是很耗時的,甚至會抵消由避免虛擬DOM比較所帶來的效能收益。React官方推薦使用immutable的組件狀態,以便更高效的實現shouldComponentUpdate函數。
immutable的狀態有何優勢呢?假設我們要修改一個列表中,某個清單項目的狀態,使用非immutable的方式:
var item = { id:1, text:‘todo1‘, status:‘doing‘}var oldTodoList = [item1,item2,....,itemn];oldTodoList[n-1].status = ‘done‘;var newTodoList = oldTotoList;
當我們需要確認oldTodoList和newTodoList的資料是否相同時,只能遍曆列表(複雜度為O(n)),依次比較:
for(var i = 0; i < oldTodoList.length; i++){ if(isItemEqual(oldTodoList[i],newTodoList[i])){ return true; }}return false;
而如果使用immutable的方式:
var newTotoList = oldTodoList.map(function(item){ if(item.id == n-1){ return Object.assign({},item,{status:‘done‘}) }else{ return item; }});
因為每一次變動,都會建立新的對象,因此比較oldTodoList和newTodoList是否有變化時,只需要比較其對象引用即可(複雜度O(1)):
return oldTodoList == newTodoList;
我們最佳化的方向就是將
shouldComponentUpdate中所有的props和state的比較演算法複雜度降到最低
,而淺層對比(isShallowEqual)就是複雜度最低的對象比較演算法:
shouldComponentUpdate: function (nextProps, nextState) { return !isShallowEqual(this.props,nextProps) || !isShallowEqual(this.state,nextState); }
當組件的prop設state都是immutable時,shouldComponentUpdate
的實現就非常簡單了,我們可以直接使用facebook官方提供了PureRenderMixin
,它就是對組件的props和state進行淺層比較的。
var PureRenderMixin = require(‘react-addons-pure-render-mixin‘);React.createClass({ mixins: [PureRenderMixin], render: function() { return <div className={this.props.className}>foo</div>; }});
自己實現immutable化,還是很有挑戰的,我們可以藉助於第三方庫ImmutableJS,它是一個重型庫,適合於大型複雜項目;如果你的項目複雜度不是很高,可以使用seamless-immutable,它是一個更輕量級的庫,基於ES5的新特性Object.freeze來避免對象的修改,因此其只能相容實現ES5標準的瀏覽器。
3.2 理解Redux狀態傳播路徑
Redux使用一個Object Storage Service整個應用的狀態(global state
),當global state
發生變化時,狀態是如何傳遞的呢?這個問題的答案對我們理解基於redux的react應用的渲染效能最佳化至關重要。
Redux將React組件分為容器型組件和展示型組件。容器型組件一般通過connet函數產生,它訂閱了全域狀態的變化,通過mapStateToProps函數,我們可以對全域狀態進行過濾,只返回該容器型組件關注的局部狀態:
function mapStateToProps(state) { return {todos: state.todos};}module.exports = connect(mapStateToProps)(TodoApp);
每一次全域狀態變化都會調用所有容器型組件的mapStateToProps方法
,該方法返回一個常規的Javascript對象,並將其合并到容器型組件的props上。
而展示型組件不直接從global state
擷取資料,其資料來源於父組件。當容器型組件對應global state
有變化時,它會將變化傳播到其所有的子組件(一般為展示型組件)。簡單來說容器型組件與展示型組件是父子關係:
組件類型 |
資料來源 |
變化通知 |
展示型組件 |
父組件 |
父組件通知 |
容器型組件 |
全域狀態 |
監聽全域狀態 |
組件的狀態傳遞路徑,可以用一個樹形結構描述:
3.3 理解Redux的預設效能最佳化
Redux官方對容器型組件和全域狀態樹有兩個基本的假設,違背這些假設將使得Redux的預設效能最佳化無法起作用:
1. 容器型組件必須為Pure Component,即組件只依賴於state和props2. 全域狀態樹(global state)的任何變動都是immutable的
這種規範是有理由的:上文中我們提到過,每一次全域狀態發生變化,所有的容器型組件都會得到通知,而各個容器型組件需要通過shouldComponentUpdate函數來確實自己關注的局部狀態是否發生變化、自身是否需要重新渲染,預設情況下,React組件的shouldComponentUpdate總返回true,這裡貌似有一個嚴重的效能問題:全域狀態的任何變動都會使頁面中的所有組件進入更新DOM
的流程
幸運的是,用Redux官方API函數connect產生的容器型組件,預設會提供一個shouldComponentUpdate函數,其中對props和state進行了淺層比較`。如果我們不遵從Redux的immutable狀態的規範和Pure Component規範,則容器型組件預設的shouldComponentUpdate函數就是無效的了。
在遵從Redux的immutable狀態規範的情況下,當一個容器型組件的預設shouldComponentUpdate函數返回true時,則表明其對應的局部狀態發生變化,需要將狀態傳播到各個子組件,相應的所有子組件也都會進行虛擬DOM比較,以確定是否需要重新渲染。如所示,容器型組件#1
的狀態發生變化後,所有的子組件都會進行虛擬DOM比較:
由於展示型組件對全域狀態沒有感知,我們就可以使用React的常規方法對展示型進行渲染效能最佳化了
。使用小節3.1中所提到的常規React組件效能最佳化
方案,對每一個展示型組件實現shouldComponentUpdate函數:
shouldComponentUpdate: function (nextProps, nextState) { return !isShallowEqual(this.props,nextProps) || !isShallowEqual(this.state,nextState); }
我們就可以避免展示型組件多餘的虛擬DOM比較。比如當只有展示型組件#1.1
需要重新渲染時,其他同層級的組件不會進行虛擬DOM比較。比如當只有展示型組件#1.1
需要重新渲染時,其他同層級的組件不會進行虛擬DOM比較了
結語: 在容器型組件層面,Redux為我們提供了預設的效能最佳化方案;在展示型組件層面,我們可以使用常規React組件效能最佳化方案。
react+redux渲染效能最佳化原理