如果你能干的父母把你生的天生迎合這個世界十减,就是莫大的幸福了栈幸。萬一沒把你生得適應(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)蔓榄。
當完成了裝載過程的時候闹炉,用戶便擁有機會去引發(fā)界面的更新了。當用戶觸發(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)和過程
當React要比較兩棵virtual dom樹的時候,是從根節(jié)點開始遞歸往下對比的放典。在一棵樹中逝变,實際上每個節(jié)點都在某種意義上是某些節(jié)點的根節(jié)點。因此奋构,這個對比算法可以從virtual dom樹上的任何一個節(jié)點開始做比較壳影。對于調(diào)和算法來說,首先他從根節(jié)點的類型開始作比較弥臼。對于這一步宴咧,具有兩個結(jié)果:根節(jié)點的類型是相同的;根節(jié)點的類型是不同的醋火。不同的結(jié)果悠汽,對于調(diào)和算法關(guān)于兩個節(jié)點是否相同會有不同的看法。
3.如果根節(jié)點的類型不同的話
如果根節(jié)點的類型不同的話芥驳,那么react會認為兩個virtual dom樹之間的改變實在是太大了柿冲,會將所有與這個根節(jié)點有關(guān)的子節(jié)點都認為是不同的,因此這些無辜者都會被拋棄兆旬。那么此時將會經(jīng)歷哪些操作呢假抄?答案就是這些舊節(jié)點的卸載以及新節(jié)點的掛載過程,注意對于此時的這種情況來說丽猬,盡管進入的頁面的更新過程宿饱,但是對于react組件來說卻不會進入組件的更新生命周期。
舉個例子:
//before
<div>
<appHeader />
<appBody />
<appFooter />
</div>
//after
<section>
<appHeader />
<appBody />
<appFooter />
</section>
對于上面這個例子來說脚祟,我們將無實質(zhì)性作用的包裹元素由div元素改為了section元素谬以,對于這種情況來說,react會把舊的相關(guān)子節(jié)點(當然也包括根節(jié)點自己)給卸載由桌,接著將新的節(jié)點給掛載到相應(yīng)的位置上去为黎。很顯然的邮丰,這里實質(zhì)性的內(nèi)容appHeader等并沒有發(fā)生變化,但是卻還是被強制卸載掛載了一波铭乾。
那么如何避免上述這種情況呢剪廉?答案是沒有,我們能做就只是避免無意義的更改元素的類型炕檩。難道不能通過設(shè)置shouldComponentUpdate來避免這種情況嗎斗蒋?答案是當然不能,因為這種情況下根本就不會進入組件的更新生命周期啊笛质。
4.如果根節(jié)點的類型相同的話
注意泉沾,但我們比較根節(jié)點的類型的時候是不會比較節(jié)點的屬性的。如果react認為根節(jié)點的類型是相同的話经瓷,那么此時調(diào)和過程將會認為可以重用某些內(nèi)容爆哑,因此不會像上述情況中所提到的大刀闊斧的經(jīng)歷卸載過程以及裝載過程,而是只是對組件進行更新過程舆吮。
具體一點描述的話揭朝,我們知道對于react來說,元素分為兩類:dom元素色冀,組件元素潭袱。當根節(jié)點是dom元素的時候,并且節(jié)點類型相同的話锋恬,那么react會比較dom元素上的屬性以及content屯换,如果發(fā)生變化的話,那么將只會在dom上修改相應(yīng)的部分与学,不會多做某些不必要的更改彤悔。
如果根節(jié)點是組件元素的話,并且節(jié)點類型相同的話索守,那么此時react會比較兩個組件元素上props的不同晕窑,利用新得到的props去更新原來的組件實例,引發(fā)這個組件的更新過程卵佛。
更新過程的生命周期函數(shù)杨赤,我們在前面也提到過,這里在溫習(xí)一下:
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
在更新過程中截汪,如果我們的shouldComponentUpdate返回false的話疾牲,那么下面的生命周期函數(shù)就不會得到執(zhí)行了,這些函數(shù)當然包括了那個挺消耗性能的render函數(shù)衙解,因此對于shouldComponentUpdate來說阳柔,他就是掌握了react組件優(yōu)化的生殺大權(quán)(當然,限于調(diào)和過程認為根節(jié)點的類型是相同的情況下)蚓峦。
在這種情況下盔沫,react會接著處理當前根節(jié)點的子節(jié)點医咨,把它們理解為根節(jié)點接著進行同樣的處理。
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處理上面這種情況很出乎意料:他不會認為是新添加了一個text props為“c“的messageItem插入到了第一位架诞,而是認為原有的text props為"a"的messageitem的props被修改為了"c",并且認為原有的text props為"b"的messageitem的props被修改為了"a"干茉,接著新增了一個text props為"b"的messageitem谴忧。
很奇怪吧,而這種做法帶來了這種問題:那就是原有的無辜的sibling都會進行更新過程(畢竟props都發(fā)生了變化能不更新嗎角虫?)沾谓,而新增得sibling那就是必備的掛載過程了。問題是這里造成了不必要的性能浪費戳鹅,畢竟那些其余sibling的內(nèi)容是沒有發(fā)生絲毫變化的均驶。
那么問題來了,如何避免這種情況呢枫虏?答案就是利用react 提供組件的key屬性妇穴,這個key屬性會被react理解為一個react組件的標志,只要你給上述情況的每一個messageItem元素都給添加了一個獨一無二的key屬性的話隶债,那么我們給messageItem組件設(shè)置的shouldComponentUpdate函數(shù)就能夠如期而至的起作用了腾它。
6.需要注意的地方
當使用key屬性的時候,我們必須給他設(shè)置一個獨一無二的值死讹;其次把數(shù)組的每一項的index設(shè)置為key的值也是錯誤的做法瞒滴。