解析React 虛擬DOM和Diff算法

一、 JSX

眾所周知形入,React 使用 JSX 來替代常規(guī)的 JavaScript。JSX 是一個看起來很像 XML 的 JavaScript 語法擴展缝左,其本質(zhì)是 createElement()方法的語法糖 (語法糖:更加直觀亿遂、簡潔、友好)渺杉。

JSX 代碼會經(jīng)過babel-loader 會解析為 React.createElement()嵌套對象蛇数。React.createElement() 創(chuàng)建的就是一個虛擬DOM結(jié)構(gòu)。

二是越、 虛擬DOM

通過React.createElement()創(chuàng)建的虛擬DOM描述了DOM樹的結(jié)構(gòu)耳舅,其本質(zhì)是一個輕量級的javaScript對象。該JS對象包含如下屬性:

- type:元素的類型倚评,可以是原生html類型(字符串)浦徊,或者自定義組件(函數(shù)或class)

- key:組件的唯一標識,用于Diff算法天梧,之后會詳細介紹

- ref:用于訪問原生dom節(jié)點

-? props:傳入組件的props盔性,children是props中的一個屬性,它存儲了當前組件的子節(jié)點呢岗,可以是數(shù)組(多個子節(jié)點)或?qū)ο螅ㄖ挥幸粋€子節(jié)點)

- owner:當前正在構(gòu)建的Component所屬的Component

- self:(非生產(chǎn)環(huán)境)指定當前位于哪個組件實例

-? _source:(非生產(chǎn)環(huán)境)指定調(diào)試代碼來自的文件(fileName)和代碼行數(shù)(lineNumber)

為更好理解冕香,下面我們來看一組轉(zhuǎn)換流程。

存在JSX代碼如下:

const element = (

? <div className="title">

? ? <span>Hello JSX</span>

? ? <ul>

? ? ? <li>test1</li>

? ? ? <li>test2</li>

? ? </ul>

? </div>

);

通過babel-loader 解析后:

const element = React.createElement(

? "div",

? { className: "title" },

? React.createElement("span", null, "Hello JSX"),

? React.createElement(

? ? "ul",

? ? null,

? ? React.createElement("li", null, "test1"),

? ? React.createElement("li", null, "test2")

? )

);

轉(zhuǎn)換成虛擬Dom后后豫,會變成如下JS代碼(為方便查看悉尾,刪除部分不必要屬性)

const element = {

? type: "div",

? props: { class: "title" },

? children: [

? ? { type: "span", children: "Hello JSX" },

? ? {

? ? ? type: "ul",

? ? ? children: [

? ? ? ? { type: "li", children: "test1" },

? ? ? ? { type: "li", children: "test2" },

? ? ? ],

? ? },

? ],

};

完整的虛擬Dom代碼如下:

由此可見,虛擬DOM就是JS對象挫酿。最后构眯,ReactDom.render 將生成好的虛擬DOM渲染到指定容器上,其中采用了批處理饭豹,事務等機制鸵赖,并且對特定瀏覽器進行了性能優(yōu)化务漩,最終轉(zhuǎn)換為真實DOM。

1. 為什么需要虛擬DOM呢?

- 提高性能

我們都知道它褪,每次DOM操作會引起重繪或者回流饵骨,頻繁的真實DOM的修改會觸發(fā)多次的排版和重繪,相當耗性能茫打。

虛擬DOM可以提高性能居触,不是說不操作DOM,而是減少操作真實DOM的次數(shù)老赤。即當狀態(tài)/數(shù)據(jù)改變時轮洋,React會自動更新虛擬DOM,產(chǎn)生一個新的虛擬DOM樹抬旺。通過diff算法對新舊虛擬DOM進行比較弊予,找出最小的有變化的部分,將這個變化的部分Patch(即需要修改的部分)加入隊列开财,最終汉柒,批量的更新這些Patch到真實DOM上,以減少重繪和回流责鳍,從而達到性能優(yōu)化的目的碾褂。

此外,React還提供了componentShouldUpdate生命周期來讓開發(fā)者手動控制減少數(shù)據(jù)變化后不必要的虛擬dom對比历葛,提升性能和渲染效率正塌。

- 跨瀏覽器兼容

React 基于 虛擬DOM自己實現(xiàn)了一套事件機制,自己模擬了事件冒泡和捕獲的過程恤溶,采用了事件代理乓诽,批量更新等方法,抹平了各個瀏覽器的事件兼容性宏娄。

- 跨平臺兼容

虛擬DOM為React帶來了跨平臺渲染的能力问裕,以React-native為例子,React根據(jù)虛擬DOM畫出相應平臺的UI.

- 提高開發(fā)效率

三孵坚、 Diff算法

傳統(tǒng)的diff算法是使用遞歸循環(huán)對節(jié)點進行依次對比粮宛,即使在最前沿的算法中 將前后兩棵樹完全比對的算法的復雜程度為 O(n^3),其中 n 是樹中元素的數(shù)量卖宠。 如果在React中使用了該算法巍杈,那么展示1000個元素所需要執(zhí)行的計算量將在十億的量級范圍。 這個開銷實在是太過高昂扛伍。

為了提高性能筷畦,React同時維護著兩棵虛擬DOM樹:一棵為當前的DOM結(jié)構(gòu)(舊虛擬DOM),另一棵為React狀態(tài)變更后生成的DOM結(jié)構(gòu)(新虛擬DOM)。React通過比較這兩棵樹的差異鳖宾,決定是否需要修改DOM結(jié)構(gòu)吼砂,以及如何修改。這種算法稱作**React 的 Diff算法**

React的 Diff算法會幫助我們計算出虛擬DOM 中真正發(fā)生變化的部分鼎文,并且只針對該部分進行實際的DOM操作渔肩,而不是對整個頁面進行重新渲染。為了降低算法復雜度拇惋,React的 Diff算法提出三種策略:

- 針對同一層級的節(jié)點進行比較周偎。即如果一個DOM節(jié)點在前后兩次更新中跨越了層級,那么React不會嘗試復用它(因為跨層級的DOM移動操作特別少撑帖,可以忽略不計)蓉坎。

- 不同類型的元素會產(chǎn)生出不同的樹。即相同類的兩個組件將會生成相似的樹形結(jié)構(gòu)胡嘿,不同類的兩個組件將會生成不同的樹形結(jié)構(gòu)蛉艾。如果元素由div變?yōu)閜,React會銷毀div及其子孫節(jié)點灶平,并新建p及其子孫節(jié)點伺通。

- 同一層級的一組子節(jié)點,可以通過唯一的id區(qū)分(key)

基于以上三個策略逢享,React 的 Diff算法分別對Tree Diff、Component Diff以及Element diff進行了算法優(yōu)化吴藻。

?1. Tree Diff

基于第一個策略瞒爬,React只會對同一層次的節(jié)點進行比較,即如果父節(jié)點不同沟堡,React將不會再去對比子節(jié)點侧但。因為不同的組件DOM結(jié)構(gòu)會不相同,所以就沒有必要再去對比子節(jié)點了航罗。這樣就只需要遍歷一次禀横,就能完成對整個DOM樹的比較,進而提高了對比的效率粥血,把事件復雜度降低為O(n).


React對于不同層級的節(jié)點柏锄,只有創(chuàng)建和刪除操作。如圖所示复亏,如果A節(jié)點整個被移動到D節(jié)點下趾娃,當根節(jié)點發(fā)現(xiàn)子節(jié)點中A不見了,就會直接銷毀A缔御;而當D發(fā)現(xiàn)自己多了一個子節(jié)點A抬闷,則會創(chuàng)建一個新的A作為子節(jié)點。


因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實際操作是:

A.destroy();

A = new A();

A.append(new B());

A.append(new C());

D.append(A);

由于React 的Diff 算法沒有針對跨層級的DOM移動操作進行深入比較耕突,對于節(jié)點跨層級移動時笤成,只是進行簡單的創(chuàng)建和刪除评架。這會影響 React 性能的操作,因此炕泳,官方建議不要進行 DOM 節(jié)點跨層級的操作纵诞。在組件開發(fā)時,推薦通過 CSS 隱藏或顯示節(jié)點喊崖,不做真正地移除或添加 DOM 節(jié)點的操作挣磨,進而保證穩(wěn)定的 DOM 結(jié)構(gòu),提升性能荤懂。

2. Component Diff

Component Diff是專門針對更新前后的同一層級間的React組件比較的Diff 算法茁裙。React對于組件間的比較采取的策略如下:

- 如果是同一類型的組件,按照原策略繼續(xù)進行虛擬DOM 比較节仿。

- 如果不是晤锥,則將該組件判斷為dirty component,從而替換整個組件下的所有子節(jié)點, 即銷毀原組件廊宪,創(chuàng)建新組件矾瘾。

- 對于同一類型的組件,有可能其虛擬DOM沒有任何變化箭启,如果能夠確切的知道這點那可以節(jié)省大量的Diff運算的時間壕翩,因此,React允許用戶通過shouldComponentUpdate()判斷該組件是否需要進行diff 算法分析傅寡。

舉個例子來說放妈,當下圖中componentD改變?yōu)閏omponentG時,即使這兩個compoent結(jié)構(gòu)很相似荐操,但是react會判斷D和G并不是同類型組件芜抒,也就不會比較二者的結(jié)構(gòu)了,而是直接刪除了D托启,重新創(chuàng)建G及其子節(jié)點宅倒。

因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實際操作是:

D.destroy();

G = new G();

G.append(new E());

G.append(new F());

V.append(G);

3. Element Diff

Element Diff是專門針對同一層級的所有節(jié)點(包括元素節(jié)點和組件節(jié)點)的Diff算法。當節(jié)點處于同一層級時屯耸,React的Diff提供了三種節(jié)點操作:插入拐迁、移動和刪除。

- 插入:新的component類型不在老集合里肩民,即全新的節(jié)點唠亚,需要對新節(jié)點執(zhí)行插入操作

- 移動:在老集合里有新component類型,且element是可更新的類型持痰,generateComponentChildren已調(diào)用receiveComponent灶搜,這種情況下prevChild=nextChild,就可以復用以前的DOM節(jié)點,執(zhí)行移動操作割卖。

- 刪除:當老的component類型前酿,在新集合中也有,但對應的element不同則不能直接復用和更新鹏溯,需要執(zhí)行刪除操作罢维;當老component不在新集合里,也需要執(zhí)行刪除操作丙挽。

如下圖肺孵,老集合為節(jié)點A、B颜阐、C平窘、D,想生成新集合B凳怨、A瑰艘、D、C肤舞,通過新老集合差異化對比紫新,最簡單粗暴的方法為: 發(fā)現(xiàn)B != A,則新集合創(chuàng)建B李剖,老集合刪除A芒率;以此類推,在老集合刪除B篙顺、C敲董、D,在新集合添加A、D慰安、C。

可以發(fā)現(xiàn)這類操作煩瑣冗余聪铺,因為這些都是相同的節(jié)點化焕,只是由于位置順序發(fā)生變化,就需要進行繁雜低效的刪除铃剔、創(chuàng)建操作撒桨,其實只要對這些節(jié)點執(zhí)行移動操作即可。為此键兜,react提出了優(yōu)化機制 ---? Key機制

四凤类、 Key機制

React允許開發(fā)者對同一層級的同組子節(jié)點,添加唯一key進行區(qū)分普气。React會根據(jù)key來決定是刪除重新創(chuàng)建組件還是更新(移動)組件谜疤,原則是:

- key相同,組件有所變化,React會只更新組件對應變化的屬性夷磕。

- key不同履肃,組件會銷毀之前的組件,將整個組件重新渲染坐桩。

1. 移動規(guī)則

添加了key 之后尺棋,按如下步驟確認是否移動: 首先,對新集合中的節(jié)點進行循環(huán)遍歷 for (name in nextChildren)绵跷,通過唯一的 key 判斷新舊集合中是否存在相同的節(jié)點膘螟,if (prevChild === nextChild)。如果存在相同節(jié)點碾局,且child.mountIndex(當前節(jié)點在老集合中的位置)與 lastIndex(參考位置荆残,類似浮標)進行比較滿足 child._mountIndex < lastIndex,則進行移動操作擦俐,否則不執(zhí)行移動操作脊阴。

這是一種順序優(yōu)化手段,lastIndex = Math.max(prevChild.mountIndex, lastIndex) 將一直更新蚯瞧,表示訪問過的節(jié)點在老集合中最右的位置(即最大的位置)嘿期,如果新集合中當前訪問的節(jié)點比 lastIndex 大,說明當前訪問節(jié)點在老集合中就比上一個節(jié)點位置靠后埋合,則該節(jié)點不會影響其他節(jié)點的位置备徐,因此不用添加到差異隊列中,即不執(zhí)行移動操作甚颂,只有當訪問的節(jié)點比 lastIndex 小時蜜猾,才需要進行移動操作。

基于移動規(guī)則振诬,我們看幾個實例:

實例(1):同一層級的所有節(jié)點只發(fā)生了位置變化

按新集合中順序開始遍歷:

1. B在新集合中 lastIndex = 0, 在舊集合中 mountIndex = 1蹭睡,mountIndex > lastIndex 就認為 B 對于集合中其他元素位置無影響,不進行移動赶么。此時肩豁,lastIndex = max(prevChild.mountIndex, lastIndex) = 1,其中辫呻,prevChild.mountIndex表示B在老集合中的位置清钥。

2. A在舊集合中 mountIndex = 0, 此時, 滿足 mountIndex < lastIndex, 則對A進行移動操作放闺。此時祟昭,lastIndex = max(prevChild.mountIndex, lastIndex) = 1。

3. D和B操作相同怖侦,同(1)篡悟,不進行移動谜叹,此時lastIndex = 3。

4. C和A操作相同恰力,同(2)叉谜,進行移動,此時lastIndex = 3踩萎。

上述結(jié)論中的移動操作即對節(jié)點進行更新渲染停局,而不進行移動則表示無需更新渲染∠愀可見有key值后董栽,相比于之前的繁瑣冗余做法,極大的提升React 的性能企孩。

實例(2): 同一層級的節(jié)點發(fā)生了節(jié)點增刪和節(jié)點位置變化

按新锭碳、老集合中順序開始遍歷:

1. 同上面那種情形,B不進行移動勿璃,lastIndex=1擒抛。

2. 新集合中取得E,發(fā)現(xiàn)舊中不存在E,在 lastIndex處創(chuàng)建E补疑,lastIndex++歧沪。

3. 在舊集合中取到C,C不移動莲组,lastIndex=2诊胞。

4. 在舊集合中取到A,A移動到新集合中的位置锹杈,lastIndex=2撵孤。

5. 完成新集合中所有節(jié)點diff后,對老集合進行循環(huán)遍歷竭望,尋找新集合中不存在但老集合中的節(jié)點(此例中為D)邪码,刪除D節(jié)點。

2. key值的缺陷

如圖所示咬清,若新集合的節(jié)點更新為 D霞扬、A、 B枫振、C,與舊集合相比只有 D 節(jié)點移動萤彩,而 A粪滤、B、C 仍然保持原有的順序雀扶,理論上 diff 應該只需對 D 執(zhí)行移動操作杖小,然而由于 D 在舊集合中的位置是最大的肆汹,導致其他節(jié)點的 mountIndex < lastIndex,造成 D 沒有執(zhí)行移動操作予权,而是 A昂勉、B、C 全部移動到 D 節(jié)點后面的現(xiàn)象扫腺。

因此岗照,在開發(fā)過程中,盡量減少類似將最后一個節(jié)點移動到列表首部的操作笆环。因為當節(jié)點數(shù)量過大或更新操作過于頻繁時攒至,這在一定程度上會影響 React 的渲染性能。

3. key值設置

如果沒有添加唯一的key值時躁劣,會遇到這個錯:

這是React在遇到列表時卻又找不到key時提示的警告迫吐。雖然無視這條警告大部分界面也會正確工作,但這通常意味著潛在的性能問題账忘,因為React覺得自己可能無法高效的去更新這個列表志膀。

同時,key值必須是穩(wěn)定的(不能使用Math.random去創(chuàng)建key), 可預測并且是唯一的鳖擒,且React官方建議不要用遍歷的index作為這種場景下的節(jié)點的key屬性值溉浙。因為使用index作key的情況時,如果當前遍歷的所有節(jié)點類型都相同败去、內(nèi)部文本不同放航,當我們對原始的數(shù)據(jù)list進行了某些元素的順序改變操作,則會導致新舊集合中進行diff比較時圆裕,相同index所對應的新舊的節(jié)點的文本不一致了广鳍,促使一些節(jié)點需要更新渲染文本。而如果用了其他穩(wěn)定的唯一標識符作為key吓妆,則只會發(fā)生位置順序變化赊时,無需更新渲染文本,提升了性能行拢。

此外祖秒,使用index作為key很可能會存在一些出人意料的顯示錯誤的問題。例如舟奠;存在三個input輸入框竭缝,以index作為其key進行渲染時。

若想實現(xiàn)點擊第二個刪除按鈕沼瘫,刪除第二列抬纸。則會發(fā)現(xiàn),第二列未成功刪除耿戚,第三列被刪除掉了湿故。

為什么呢阿趁?

這是因為你認為你刪除了2,但React會認為你做了兩件事:「把2變成了3」以及「把3刪除了」坛猪。

看看這兩個數(shù)組:[123]和[13]脖阵,人類會說,這不就是少了個2嗎墅茉?但是計算機會遍歷數(shù)組:首先對比1和1命黔,發(fā)現(xiàn)1沒變;然后對比2和3躁锁,發(fā)現(xiàn)2變成了3; 最后對比undefined和3纷铣,發(fā)現(xiàn)「3被刪除了」。所以計算機的結(jié)論是:「2變成了3」以及「3被刪除了」战转。

因此搜立,React渲染邏輯為: 1沒變,復用之前的1和三角形槐秧;「2變成了3」啄踊,正方形左邊的2改為3。里面的正方形就地復用(正方形沒有被刪除)刁标;「3被刪除了」颠通,之前的「圓形」當然應該被刪掉,里面的子元素也要刪除膀懈。

因此顿锰,為了避免此類型錯誤,也為了提升性能启搂,不要使用index作為key值硼控。

# 參考內(nèi)容:

[React 源碼剖析系列]

[虛擬DOM與DOM Diff 的原理]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胳赌,隨后出現(xiàn)的幾起案子牢撼,更是在濱河造成了極大的恐慌,老刑警劉巖疑苫,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熏版,死亡現(xiàn)場離奇詭異,居然都是意外死亡捍掺,警方通過查閱死者的電腦和手機撼短,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挺勿,“玉大人阔加,你說我怎么就攤上這事÷樱” “怎么了胜榔?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長湃番。 經(jīng)常有香客問我夭织,道長,這世上最難降的妖魔是什么吠撮? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任尊惰,我火速辦了婚禮,結(jié)果婚禮上泥兰,老公的妹妹穿的比我還像新娘弄屡。我一直安慰自己,他們只是感情好鞋诗,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布膀捷。 她就那樣靜靜地躺著,像睡著了一般削彬。 火紅的嫁衣襯著肌膚如雪全庸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天融痛,我揣著相機與錄音壶笼,去河邊找鬼。 笑死雁刷,一個胖子當著我的面吹牛覆劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沛励,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼责语,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侯勉?” 一聲冷哼從身側(cè)響起鹦筹,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎址貌,沒想到半個月后铐拐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡练对,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年遍蟋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片螟凭。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡虚青,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出螺男,到底是詐尸還是另有隱情棒厘,我是刑警寧澤纵穿,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站奢人,受9級特大地震影響谓媒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜何乎,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一句惯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧支救,春花似錦抢野、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至欲主,卻和暖如春邓厕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扁瓢。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工详恼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人引几。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓昧互,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伟桅。 傳聞我的和親對象是個殘疾皇子敞掘,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • diff算法作為Virtual DOM的加速器,其算法的改進優(yōu)化是React整個界面渲染的基礎和性能的保障楣铁,同時也...
    指尖跳動閱讀 1,245評論 0 1
  • 為何采用虛擬DOM 尤雨溪曾在知乎正面的回答這個問題: 為函數(shù)式的 UI 編程方式打開了大門玖雁;可以渲染到 DOM ...
    yiludege閱讀 2,507評論 0 4
  • 一雌桑、diff策略 1.Web UI中DOM節(jié)點跨層級的移動特別少垃瞧,可以忽略不計 2.擁有相同類的兩個組件將會生成相...
    南慕瑤閱讀 5,321評論 0 0
  • 原文:https://segmentfault.com/a/1190000010686582 React框架使用的...
    宋00閱讀 764評論 0 0
  • 什么是diff算法 react 作為一款最主流的前端框架之一雪隧,在設計的時候除了簡化操作之外啊奄,最注重的地方就是節(jié)省性...
    鶴仔z閱讀 1,224評論 0 7