性能優化
每當開發者選擇將React用在真實項目中時都會先問一個問題:使用react是否會讓項目速度更快,更靈活,更容易維護。此外每次狀態數據發生改變時都會進行重新渲染界面的處理做法會不會造成性能瓶頸?而在react內部則是通過使用一些精妙的技巧來最小化每次造成ui更新的昂貴的dom操作從而保證性能的。
避免直接作用於DOM
react實現了一層虛擬dom,它用來映射瀏覽器的原生dom樹。通過這一層虛擬的dom,可以讓react避免直接操作dom,因為直接操作瀏覽器dom的速度要遠低於操作JavaScript對象。每當組件的屬性或者狀態發生改變時,react會在內存中構造一個新的虛擬dom與原先老的進行對比,用來判斷是否需要更新瀏覽器的dom樹,這樣就盡可能的優化了渲染dom的性能損耗。
在此之上,react提供了組件生命周期函數,shouldComponentUpdate
,組件在決定重新渲染(虛擬dom比對完畢生成最終的dom后)之前會調用該函數,該函數將是否重新渲染的權限交給了開發者,該函數默認直接返回true
,表示默認直接出發dom更新:
shouldComponentUpdate: function(nextProps, nextState) { return true; }
值得注意的是,react會非常頻繁的調用該函數,所以如果你打算自己實現該函數的邏輯,請盡可能保證性能。
比方說,你有一個擁有多個帖子的聊天應用,如果此時只有一個發生了變化,如果你如下實現了shouldComponentUpdate
,react會根據情況避免重新渲染那些沒有發生變化的帖子:
shouldComponentUpdate: function(nextProps, nextState) { // TODO: return whether or not current chat thread is different to former one. // 根據實際情況判斷當前帖子的狀態是否和之前不同 }
總之,react盡可能的避免了昂貴的dom操作,並且允許開發者干涉該行為。
shouldComponentUpdate實戰
這里舉個包含子元素的組件例子,如下圖:
圖中每個圓點表示一個dom節點,當某個dom節點的shouldComponentUpdate
返回false
時(例如c2),react就無需為其更新dom,注意,react甚至根本不會去調用c4和c5節點的shouldComponentUpdate
函數哦~
圖中c1和c3的shouldComponentUpdate
返回了true
,因此react會檢查檢查其它們包含的直接子節點。最有趣的是c8節點,雖然調用它的shouldComponentUpdate
方法返回的是true
,但react檢查后發現其dom結構並未發生改變,所以react最終是不會重新渲染其瀏覽器dom的。
上圖的情況下,react最終只會重新渲染c6,原因你應該懂的。
那么我們應該如何實現shouldComponentUpdate
函數呢?假設你有一個只包含字符串的組件,如下:
React.createClass({ propTypes: { value: React.PropTypes.string.isRequired }, render: function() { return <div>{this.props.value}</div>; } });
我們可以簡單的直接實現shouldComponentUpdate
如下:
shouldComponentUpdate: function(nextProps, nextState) { return this.props.value !== nextProps.value; }
目前為止一切都很順利,處理基礎類型的屬性和狀態是很簡單的,我們可以直接使用js語言提供的===
比對來實現一個mix並注入到所有組件中,事實上,react自身已經提供了一個類似的:PureRenderMixin。
但是如果你的組件所擁有的屬性或狀態不是基礎類型呢,而是復合類型呢?比方說是一個js對象,{foo: 'bar'}
:
React.createClass({
propTypes: {
value: React.PropTypes.object.isRequired }, render: function() { return <div>{this.props.value.foo}</div>; } });
這種情況下我們剛才實現的那種shouldComponentUpdate
就歇菜了:
// 假設 this.props.value 是 { foo: 'bar' } // 假設 nextProps.value 是 { foo: 'bar' }, // 但是nextProps和this.props對應的引用不相同 this.props.value !== nextProps.value; // true
要想修復這個問題,簡單粗暴的方法是我們直接比對foo
的值,如下:
shouldComponentUpdate: function(nextProps, nextState) { return this.props.value.foo !== nextProps.value.foo; }
我們當然可以通過深比對來確定屬性或狀態是否確實發生了改變,但是這種深比對是非常昂貴的,還記得我們剛出說過shouldComponentUpdate
函數的調用非常頻繁么?更何況我們為每個model去單獨實現一個匹配的深比對邏輯,對於開發人員來說也是非常痛苦的。最重要的是,如果我們不是很小心的處理對象引用關系的話,還會帶來災難。例如下面這個組件:
React.createClass({
getInitialState: function() { return { value: { foo: 'bar' } }; }, onClick: function() { var value = this.state.value; value.foo += 'bar'; // ANTI-PATTERN! this.setState({ value: value }); }, render: function() { return ( <div> <InnerComponent value={this.state.value} /> <a onClick={this.onClick}>Click me</a> </div> ); } });
起初,InnerComponent組件進行渲染,它得到的value屬性為{foo: 'bar'}
。當用戶點擊鏈接后,父組件的狀態將會更新為{ value: { foo: 'barbar' } }
,觸發了InnerComponent組件的重新渲染,因為它得到了一個新的屬性:{ foo: 'barbar' }
。
看上去一切都挺好的,其實問題在於,父組件和子組件供用了同一個對象的引用,當用戶觸發click事件時,InnerComponent的prop將會發生改變,因此它的shouldComponentUpdate
函數將會被調用,而此時如果按照我們目前的shouldComponentUpdate
比對邏輯的話,this.props.value.foo
和nextProps.value.foo
是相等的,因為事實上,它們同時引用同一個對象哦~所以,我們將會看到,InnerComponent的ui並沒有更新。哎~,不信的話,我貼出完整代碼:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>demo</title> <!--引入React庫--> <script src="lib/react.min.js"></script> <!--引入JSX轉換庫--> <script src="lib/JSXTransformer.js"></script> <!--組件樣式--> </head> <body> <!--定義容器--> <div id="content"></div> <!--聲明腳本類型為JSX--> <script type="text/jsx"> var InnerComponent = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { return this.props.value.foo !== nextProps.value.foo; }, render: function() { return ( <div> {this.props.value.foo} </div> ); } }); var OutComponent = React.createClass({ getInitialState: function() { return { value: { foo: 'bar' } }; }, onClick: function() { var value = this.state.value; value.foo += 'bar'; // ANTI-PATTERN! this.setState({ value: value }); }, render: function() { return ( <div> <InnerComponent value={this.state.value} /> <a onClick={this.onClick}>Click me</a> </div> ); } }); React.render(<OutComponent />, document.querySelector("#content")); </script> </body> </html>