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