如果你能干的父母把你生的天生迎合這個世界,就是莫大的幸福了。萬一沒把你生得適應(yīng)這個世界,那么要么一直忍氣吞聲,要么韜光養(yǎng)晦直至適應(yīng),沒有別的路可走。 ——《我是貓》
1.概述
React通過render方法在內(nèi)存中產(chǎn)生一個樹形virtual dom結(jié)構(gòu),這個virtual dom結(jié)構(gòu)會被react處理為一個瀏覽器接受的DOM樹。正是virtual dom的引入,使得react更新性能能夠得到一個較好的表現(xiàn)。
當(dāng)完成了裝載過程的時候,用戶便擁有機(jī)會去引發(fā)界面的更新了。當(dāng)用戶觸發(fā)了界面更新的時候,此時react仍然通過render方法去生成一個新的virtual dom,注意是生成了整棵virtual dom樹,而不是引起變化的那部分。那么接下來的操作難道是利用整棵virtual dom樹轉(zhuǎn)化成DOM樹以供瀏覽器重新渲染嗎?在初次掛載過程自然是這樣的。但是其它情況下那就不是這樣了,畢竟雖然觸發(fā)了頁面的更新過程,但是引起更新的根源有可能就只是頁面的一小部分,因此在頁面發(fā)生更新時直接拿新生成的virtual dom去生成dom樹是非常不應(yīng)該的,至少在性能方面是不能接受的。
那么React在更新過程又是怎么盡力去避免性能問題的呢?答案就是react會提供一個“調(diào)和”過程,這個調(diào)優(yōu)過程會對比新的virtual dom樹以及舊的virtual dom樹,接著找出兩者所不同的地方,根據(jù)不同的地方來修改現(xiàn)有的DOM。那么這個調(diào)優(yōu)過程判具體又是如何判斷兩棵virtual dom樹是不同的呢?
2.調(diào)和過程
當(dāng)React要比較兩棵virtual dom樹的時候,是從根節(jié)點(diǎn)開始遞歸往下對比的。在一棵樹中,實(shí)際上每個節(jié)點(diǎn)都在某種意義上是某些節(jié)點(diǎn)的根節(jié)點(diǎn)。因此,這個對比算法可以從virtual dom樹上的任何一個節(jié)點(diǎn)開始做比較。對于調(diào)和算法來說,首先他從根節(jié)點(diǎn)的類型開始作比較。對于這一步,具有兩個結(jié)果:根節(jié)點(diǎn)的類型是相同的;根節(jié)點(diǎn)的類型是不同的。不同的結(jié)果,對于調(diào)和算法關(guān)于兩個節(jié)點(diǎn)是否相同會有不同的看法。
3.如果根節(jié)點(diǎn)的類型不同的話
如果根節(jié)點(diǎn)的類型不同的話,那么react會認(rèn)為兩個virtual dom樹之間的改變實(shí)在是太大了,會將所有與這個根節(jié)點(diǎn)有關(guān)的子節(jié)點(diǎn)都認(rèn)為是不同的,因此這些無辜者都會被拋棄。那么此時將會經(jīng)歷哪些操作呢?答案就是這些舊節(jié)點(diǎn)的卸載以及新節(jié)點(diǎn)的掛載過程,注意對于此時的這種情況來說,盡管進(jìn)入的頁面的更新過程,但是對于react組件來說卻不會進(jìn)入組件的更新生命周期。
舉個例子:
//before
<div>
<appHeader />
<appBody />
<appFooter />
</div>
//after
<section>
<appHeader />
<appBody />
<appFooter />
</section>
對于上面這個例子來說,我們將無實(shí)質(zhì)性作用的包裹元素由div元素改為了section元素,對于這種情況來說,react會把舊的相關(guān)子節(jié)點(diǎn)(當(dāng)然也包括根節(jié)點(diǎn)自己)給卸載,接著將新的節(jié)點(diǎn)給掛載到相應(yīng)的位置上去。很顯然的,這里實(shí)質(zhì)性的內(nèi)容appHeader等并沒有發(fā)生變化,但是卻還是被強(qiáng)制卸載掛載了一波。
那么如何避免上述這種情況呢?答案是沒有,我們能做就只是避免無意義的更改元素的類型。難道不能通過設(shè)置shouldComponentUpdate來避免這種情況嗎?答案是當(dāng)然不能,因?yàn)檫@種情況下根本就不會進(jìn)入組件的更新生命周期啊。
4.如果根節(jié)點(diǎn)的類型相同的話
注意,但我們比較根節(jié)點(diǎn)的類型的時候是不會比較節(jié)點(diǎn)的屬性的。如果react認(rèn)為根節(jié)點(diǎn)的類型是相同的話,那么此時調(diào)和過程將會認(rèn)為可以重用某些內(nèi)容,因此不會像上述情況中所提到的大刀闊斧的經(jīng)歷卸載過程以及裝載過程,而是只是對組件進(jìn)行更新過程。
具體一點(diǎn)描述的話,我們知道對于react來說,元素分為兩類:dom元素,組件元素。當(dāng)根節(jié)點(diǎn)是dom元素的時候,并且節(jié)點(diǎn)類型相同的話,那么react會比較dom元素上的屬性以及content,如果發(fā)生變化的話,那么將只會在dom上修改相應(yīng)的部分,不會多做某些不必要的更改。
如果根節(jié)點(diǎn)是組件元素的話,并且節(jié)點(diǎn)類型相同的話,那么此時react會比較兩個組件元素上props的不同,利用新得到的props去更新原來的組件實(shí)例,引發(fā)這個組件的更新過程。
更新過程的生命周期函數(shù),我們在前面也提到過,這里在溫習(xí)一下:
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
在更新過程中,如果我們的shouldComponentUpdate返回false的話,那么下面的生命周期函數(shù)就不會得到執(zhí)行了,這些函數(shù)當(dāng)然包括了那個挺消耗性能的render函數(shù),因此對于shouldComponentUpdate來說,他就是掌握了react組件優(yōu)化的生殺大權(quán)(當(dāng)然,限于調(diào)和過程認(rèn)為根節(jié)點(diǎn)的類型是相同的情況下)。
在這種情況下,react會接著處理當(dāng)前根節(jié)點(diǎn)的子節(jié)點(diǎn),把它們理解為根節(jié)點(diǎn)接著進(jìn)行同樣的處理。
5.sibling之間發(fā)生變化所引起的調(diào)和處理
在下面這種情況下:
//before
<CommentList>
<messageItem text="a" />
<messageItem text="b" />
</CommentList>
//after
<CommentList>
<messageItem text="c" />
<messageItem text="a" />
<messageItem text="b" />
</CommentList>
react處理上面這種情況很出乎意料:他不會認(rèn)為是新添加了一個text props為“c“的messageItem插入到了第一位,而是認(rèn)為原有的text props為"a"的messageitem的props被修改為了"c",并且認(rèn)為原有的text props為"b"的messageitem的props被修改為了"a",接著新增了一個text props為"b"的messageitem。
很奇怪吧,而這種做法帶來了這種問題:那就是原有的無辜的sibling都會進(jìn)行更新過程(畢竟props都發(fā)生了變化能不更新嗎?),而新增得sibling那就是必備的掛載過程了。問題是這里造成了不必要的性能浪費(fèi),畢竟那些其余sibling的內(nèi)容是沒有發(fā)生絲毫變化的。
那么問題來了,如何避免這種情況呢?答案就是利用react 提供組件的key屬性,這個key屬性會被react理解為一個react組件的標(biāo)志,只要你給上述情況的每一個messageItem元素都給添加了一個獨(dú)一無二的key屬性的話,那么我們給messageItem組件設(shè)置的shouldComponentUpdate函數(shù)就能夠如期而至的起作用了。
6.需要注意的地方
當(dāng)使用key屬性的時候,我們必須給他設(shè)置一個獨(dú)一無二的值;其次把數(shù)組的每一項(xiàng)的index設(shè)置為key的值也是錯誤的做法。