人們首先會考慮的是 React 是能否和其他非 React 版本一樣能快速和響應(yīng)一個項目。重新繪制組件的整個子樹來回應(yīng)每一個狀態(tài)變化的想法讓人懷疑是否這個過程中對性能產(chǎn)生負(fù)面影響。React 使用幾個巧妙的技巧,以減少所需的更新用戶界面所需要的昂貴的文檔用戶模型操作的數(shù)目。
React 使用了一個虛擬的 DOM,這是的瀏覽器中對于 DOM 樹呈現(xiàn)的一個描述符。這種并行表示形式讓 React 避免產(chǎn)生 DOM 節(jié)點和訪問現(xiàn)有的節(jié)點,比 JavaScript 對象的操作速度較慢。當(dāng)一個組件的道具或狀態(tài)改變,React 決定通過構(gòu)建一個新的虛擬 DOM 來進(jìn)行實際的 DOM 更新和舊的 DOM 相比是否必要。只有在比較結(jié)果不一樣的情況下,Reac t盡可能少的應(yīng)用轉(zhuǎn)變來融合文檔對象模型。
在此之上,React 提供了一個組件的生命周期功能,shouldComponentUpdate
,這是在重新繪制過程開始之前觸發(fā)(虛擬 DOM 比較,可能最終調(diào)和 DOM),使開發(fā)人員能夠減少過程中的循環(huán)步驟。這個函數(shù)的默認(rèn)實現(xiàn)返回 true
,讓 React 來執(zhí)行更新:
shouldComponentUpdate: function(nextProps, nextState) { return true; }
請記住,React 將非常頻繁的調(diào)用這個函數(shù),所以實現(xiàn)必須要快。 比如說你有一個由幾個聊天線程組成的消息應(yīng)用程序。假設(shè)只有一個線程已經(jīng)改變。如果我們在 ChatThread
上執(zhí)行 shouldComponentUpdate
,React 可以為其他線程跳過描繪步驟:
shouldComponentUpdate: function(nextProps, nextState) { // TODO: return whether or not current chat thread is // different to former one.}
因此,簡言之,React 避免調(diào)和 DOM 產(chǎn)生的復(fù)雜的 DOM 操作,允許用戶使用 shouldComponentUpdate
縮短過程中的循環(huán)步驟,而且,對于那些需要更新,通過對比虛擬的 DOM 來實現(xiàn)。
下面是組件的子樹。對于每一個組件表示 shouldComponentUpdate
的返回值,以及是否與虛擬的 DOM 是等價的。最后,圓的顏色指示組件是否必須調(diào)和。
在上面的例子中,由于 shouldComponentUpdate
返回值為 false
,存在 C2 中,React 沒有必要產(chǎn)生新的虛擬的 DOM,并且因此,也不需要調(diào)和 DOM。需要注意的是 react 甚至沒有在 C4 和 C5 處調(diào)用 shouldComponentUpdate
。
對于 C1 和 C3 shouldComponentUpdate
返回 true
,所以 React 不得不深入到葉子節(jié)點并且進(jìn)行檢查。
對于 C6 它返回 true
;由于和虛擬的 DOM 并不等同,它不得不調(diào)和 DOM。最后一個有趣的例子是 C8。此節(jié)點的 React 必須計算虛擬 DOM,但因為它和原來的 DOM 相同,它不需要調(diào)和 DOM。
請注意,只有 C6 需要 React 不得不對 DOM 做轉(zhuǎn)變,這是不可避免的。對于 C8 通過比較虛擬的 DOM 他不需要轉(zhuǎn)變,但是對 C2 的子樹和 C7來說,它甚至沒有計算虛擬 DOM,只需要通過執(zhí)行 shouldComponentUpdate
。
所以,我們應(yīng)該如何執(zhí)行 shouldComponentUpdate
呢?比如現(xiàn)有一個僅需呈現(xiàn)一個字符串值的組件:
React.createClass({ propsTypes: { value: React.PropTypes.string.isRequired }, render: function() { return <div>this.props.value</div>; } });
我們可以很容易地實現(xiàn) shouldComponentUpdate
,如下:
shouldComponentUpdate: function(nextProps, nextState) { return this.props.value !== nextProps.value; }
到目前為止,React 處理這類簡單的道具/態(tài)結(jié)構(gòu)非常簡單。我們甚至可以泛化一個基于淺層相等實現(xiàn),并混合到組件上。事實上,React 已經(jīng)提供了這樣實現(xiàn):PureRenderMixin。
但如果你的組件的道具或狀態(tài)是可變的數(shù)據(jù)結(jié)構(gòu)呢?比如說組件接受的道具,而不是像'bar'
的這樣的字符串,而是一個是包含,如,{FOO:'bar'}
這樣一個字符串的 JavaScript 對象:
React.createClass({ propsTypes: { value: React.PropTypes.object.isRequired }, render: function() { return <div>this.props.value.foo</div>; } });
我們之前的 shouldComponentUpdate
實現(xiàn)總是不會如我們預(yù)期一樣的實現(xiàn):
/ assume this.props.value is { foo: 'bar' }// assume nextProps.value is { foo: 'bar' },// but this reference is different to this.props.valuethis.props.value !== nextProps.value; // true
問題是當(dāng)?shù)谰邔嶋H上并沒有改變時,shouldComponentUpdate
將返回 true
。為了解決這個問題,我們可以用這個替代的試行方案:
shouldComponentUpdate: function(nextProps, nextState) { return this.props.value.foo !== nextProps.value.foo; }
基本上,我們?yōu)榇_保我們正確地跟蹤變化,最后做了深刻的對比。這種做法是在性能方面相當(dāng)昂貴和復(fù)雜的,它并不能擴展,因為我們要為每個模型寫不同的深度相等代碼。最重要的是,如果我們不小心管理對象的引用,它甚至可能沒有工作。比如說母節(jié)點組件的引用:
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> ); } });
在第一時間內(nèi)部組件得到呈現(xiàn)是 {FOO:'bar'}
,它將作為道具的值。如果用戶點擊,母組件的狀態(tài)將得到更新為{value:{FOO:'barbar'}}
,引發(fā)了內(nèi)部部件在過程中重新呈現(xiàn),將接收 {foo:“barbar'}
為道具的新值。
問題是,由于母體和內(nèi)部部件共享一個參考同一個對象,當(dāng)對象在第二行的 onClick
功能函數(shù)中突變時,道具的內(nèi)部部件具有將發(fā)生變化。這樣,當(dāng)再描繪處理開始時,shouldComponentUpdate
被調(diào)用,this.props.value.foo
將等于nextProps.value.foo
,因為事實上,this.props.value
和 nextProps.value
引用相同的對象。
因此,由于我們錯過了道具和縮短步驟重新渲染過程中的變化,用戶界面將不會得到從 'bar'
到 'barbar'
的更新。
Immutable-JS 是 Lee Byron 寫的腳本語言集合庫,其中 Facebook 最近開源 Javascript 的集合庫。它提供了通過結(jié)構(gòu)性共享一成不變持久化集合。讓我們看看這些性能:
不可變的:一旦創(chuàng)建,集合不能在另一個時間點改變。
持久性:新的集合可以由從早先的集合和突變結(jié)合創(chuàng)建。在創(chuàng)建新的集合后,原來集合仍然有效。
結(jié)構(gòu)共享:使用新的集合創(chuàng)建為與對原始集合大致相同的結(jié)構(gòu),減少了拷貝的最低限度,以實現(xiàn)空間效率和可接受的性能。如果新的集合等于原始的集合,則通常會返回原來的集合。
不變性使得跟蹤更改方便;而變化將總是產(chǎn)生在新的對象,所以我們只需要檢查的已經(jīng)改變參考對象。例如,在這個 Javascript 代碼中:
var x = { foo: "bar" };var y = x; y.foo = "baz"; x === y; // true
雖然 y
被修改了,但因為它是對相同對象 x
的引用,所以這個比較返回 true
。然而,這段代碼可以用 immutable-JS 這樣寫:
var SomeRecord = Immutable.Record({ foo: null });var x = new SomeRecord({ foo: 'bar' });var y = x.set('foo', 'baz'); x === y; // false
在這種情況下,由于 x
突變,當(dāng)一個新的引用被返回,我們可以安全地假設(shè) x
已經(jīng)改變。
另一種跟蹤變化的可能的方法是通過設(shè)置標(biāo)志來做 dirty 檢查。這種方法的一個問題是,它迫使你使用 setter 或者寫很多額外的代碼,或某種類工具?;蛘?,你可以突變之前深入對象復(fù)制并深入比較,以確定是否有變化。這種方法的一個問題是 deepCopy 和 deepCompare 是耗費大且復(fù)雜的操作。
因此,不可變的數(shù)據(jù)結(jié)構(gòu)為您提供了一種廉價和更簡潔的方式來跟蹤對象的變化,這就是我們?yōu)榱藢崿F(xiàn)shouldComponentUpdate
所需要的。因此,如果我們利用 immutable-JS 提供的抽象特性來支持和聲明屬性,我們就可以使用 PureRenderMixin
并獲得 perf 的一個很好的推動。
如果你使用通量,你應(yīng)該開始使用 immutable-JS 寫你的庫。看看完整的 API。
讓我們來看看使用不可變的數(shù)據(jù)結(jié)構(gòu)來模擬線程的一種可行的辦法。首先,我們需要為每一個我們正在嘗試模擬的實體定義一個 record
。記錄是不可變的容器,為一組特定的域保存值:
var User = Immutable.Record({ id: undefined, name: undefined, email: undefined});var Message = Immutable.Record({ timestamp: new Date(), sender: undefined, text: ''});
Record
函數(shù)接收的對象定義了對象的域和它們的默認(rèn)值。
消息庫可以使用以下兩個列表來跟蹤用戶和信息:
this.users = Immutable.List();this.messages = Immutable.List();
實現(xiàn)處理每個負(fù)載類型的功能應(yīng)該是相當(dāng)容易的。例如,當(dāng)庫看到負(fù)載正在顯示一個新的消息,我們只要創(chuàng)建一個新的記錄,并把它添加到消息列表中:
this.messages = this.messages.push(new Message({ timestamp: payload.timestamp, sender: payload.sender, text: payload.text });
請注意,由于數(shù)據(jù)結(jié)構(gòu)是不可改變的,我們需要把推送功能的結(jié)果分配到 this.messages 中。
在 React 中,如果我們也使用 immutable-JS 數(shù)據(jù)結(jié)構(gòu)來保存組件的狀態(tài)下,我們可以混合 PureRenderMixin
到所有的組件,并且縮短重新呈現(xiàn)的過程。
更多建議: