日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

虚拟DOM Diff算法解析

發(fā)布時(shí)間:2023/12/6 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚拟DOM Diff算法解析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

React中最神奇的部分莫過于虛擬DOM,以及其高效的Diff算法。這讓我們可以無需擔(dān)心性能問題而”毫無顧忌”的隨時(shí)“刷新”整個(gè)頁面,由虛擬DOM來確保只對界面上真正變化的部分進(jìn)行實(shí)際的DOM操作。React在這一部分已經(jīng)做到足夠透明,在實(shí)際開發(fā)中我們基本無需關(guān)心虛擬DOM是如何運(yùn)作的。然而,作為有態(tài)度的程序員,我們總是對技術(shù)背后的原理充滿著好奇。理解其運(yùn)行機(jī)制不僅有助于更好的理解React組件的生命周期,而且對于進(jìn)一步優(yōu)化React程序也會有很大幫助。

什么是DOM Diff算法

Web界面由DOM樹來構(gòu)成,當(dāng)其中某一部分發(fā)生變化時(shí),其實(shí)就是對應(yīng)的某個(gè)DOM節(jié)點(diǎn)發(fā)生了變化。在React中,構(gòu)建UI界面的思路是由當(dāng)前狀態(tài)決定界面。前后兩個(gè)狀態(tài)就對應(yīng)兩套界面,然后由React來比較兩個(gè)界面的區(qū)別,這就需要對DOM樹進(jìn)行Diff算法分析。

即給定任意兩棵樹,找到最少的轉(zhuǎn)換步驟。但是標(biāo)準(zhǔn)的的Diff算法復(fù)雜度需要O(n^3),這顯然無法滿足性能要求。要達(dá)到每次界面都可以整體刷新界面的目的,勢必需要對算法進(jìn)行優(yōu)化。這看上去非常有難度,然而Facebook工程師卻做到了,他們結(jié)合Web界面的特點(diǎn)做出了兩個(gè)簡單的假設(shè),使得Diff算法復(fù)雜度直接降低到O(n)

  • 兩個(gè)相同組件產(chǎn)生類似的DOM結(jié)構(gòu),不同的組件產(chǎn)生不同的DOM結(jié)構(gòu);

  • 對于同一層次的一組子節(jié)點(diǎn),它們可以通過唯一的id進(jìn)行區(qū)分。

  • 算法上的優(yōu)化是React整個(gè)界面Render的基礎(chǔ),事實(shí)也證明這兩個(gè)假設(shè)是合理而精確的,保證了整體界面構(gòu)建的性能。

    不同節(jié)點(diǎn)類型的比較

    為了在樹之間進(jìn)行比較,我們首先要能夠比較兩個(gè)節(jié)點(diǎn),在React中即比較兩個(gè)虛擬DOM節(jié)點(diǎn),當(dāng)兩個(gè)節(jié)點(diǎn)不同時(shí),應(yīng)該如何處理。這分為兩種情況:(1)節(jié)點(diǎn)類型不同 ,(2)節(jié)點(diǎn)類型相同,但是屬性不同。本節(jié)先看第一種情況。

    當(dāng)在樹中的同一位置前后輸出了不同類型的節(jié)點(diǎn),React直接刪除前面的節(jié)點(diǎn),然后創(chuàng)建并插入新的節(jié)點(diǎn)。假設(shè)我們在樹的同一位置前后兩次輸出不同類型的節(jié)點(diǎn)。


    renderA:?<div?/>
    renderB:?<span?/>
    =>?[removeNode?<div?/>],?[insertNode?<span?/>]


    當(dāng)一個(gè)節(jié)點(diǎn)從div變成span時(shí),簡單的直接刪除div節(jié)點(diǎn),并插入一個(gè)新的span節(jié)點(diǎn)。這符合我們對真實(shí)DOM操作的理解。

    需要注意的是,刪除節(jié)點(diǎn)意味著徹底銷毀該節(jié)點(diǎn),而不是再后續(xù)的比較中再去看是否有另外一個(gè)節(jié)點(diǎn)等同于該刪除的節(jié)點(diǎn)。如果該刪除的節(jié)點(diǎn)之下有子節(jié)點(diǎn),那么這些子節(jié)點(diǎn)也會被完全刪除,它們也不會用于后面的比較。這也是算法復(fù)雜能夠降低到O(n)的原因。

    上面提到的是對虛擬DOM節(jié)點(diǎn)的操作,而同樣的邏輯也被用在React組件的比較,例如:


    renderA:?<Header?/>
    renderB:?<Content?/>
    =>?[removeNode?<Header?/>],?[insertNode?<Content?/>]


    當(dāng)React在同一個(gè)位置遇到不同的組件時(shí),也是簡單的銷毀第一個(gè)組件,而把新創(chuàng)建的組件加上去。這正是應(yīng)用了第一個(gè)假設(shè),不同的組件一般會產(chǎn)生不一樣的DOM結(jié)構(gòu),與其浪費(fèi)時(shí)間去比較它們基本上不會等價(jià)的DOM結(jié)構(gòu),還不如完全創(chuàng)建一個(gè)新的組件加上去。

    由這一React對不同類型的節(jié)點(diǎn)的處理邏輯我們很容易得到推論,那就是React的DOM Diff算法實(shí)際上只會對樹進(jìn)行逐層比較,如下所述。

    逐層進(jìn)行節(jié)點(diǎn)比較

    提到樹,相信大多數(shù)同學(xué)立刻想到的是二叉樹,遍歷,最短路徑等復(fù)雜的數(shù)據(jù)結(jié)構(gòu)算法。而在React中,樹的算法其實(shí)非常簡單,那就是兩棵樹只會對同一層次的節(jié)點(diǎn)進(jìn)行比較。如下圖所示:

    React只會對相同顏色方框內(nèi)的DOM節(jié)點(diǎn)進(jìn)行比較,即同一個(gè)父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)。當(dāng)發(fā)現(xiàn)節(jié)點(diǎn)已經(jīng)不存在,則該節(jié)點(diǎn)及其子節(jié)點(diǎn)會被完全刪除掉,不會用于進(jìn)一步的比較。這樣只需要對樹進(jìn)行一次遍歷,便能完成整個(gè)DOM樹的比較。

    例如,考慮有下面的DOM結(jié)構(gòu)轉(zhuǎn)換:

    A節(jié)點(diǎn)被整個(gè)移動到D節(jié)點(diǎn)下,直觀的考慮DOM Diff操作應(yīng)該是


    A.parent.remove(A);?
    D.append(A);


    但因?yàn)镽eact只會簡單的考慮同層節(jié)點(diǎn)的位置變換,對于不同層的節(jié)點(diǎn),只有簡單的創(chuàng)建和刪除。當(dāng)根節(jié)點(diǎn)發(fā)現(xiàn)子節(jié)點(diǎn)中A不見了,就會直接銷毀A;而當(dāng)D發(fā)現(xiàn)自己多了一個(gè)子節(jié)點(diǎn)A,則會創(chuàng)建一個(gè)新的A作為子節(jié)點(diǎn)。因此對于這種結(jié)構(gòu)的轉(zhuǎn)變的實(shí)際操作是:


    A.destroy();A?=?new?A();A.append(new?B());A.append(new?C());D.append(A);


    可以看到,以A為根節(jié)點(diǎn)的樹被整個(gè)重新創(chuàng)建。

    雖然看上去這樣的算法有些“簡陋”,但是其基于的是第一個(gè)假設(shè):兩個(gè)不同組件一般產(chǎn)生不一樣的DOM結(jié)構(gòu)。根據(jù)React官方博客,這一假設(shè)至今為止沒有導(dǎo)致嚴(yán)重的性能問題。這當(dāng)然也給我們一個(gè)提示,在實(shí)現(xiàn)自己的組件時(shí),保持穩(wěn)定的DOM結(jié)構(gòu)會有助于性能的提升。例如,我們有時(shí)可以通過CSS隱藏或顯示某些節(jié)點(diǎn),而不是真的移除或添加DOM節(jié)點(diǎn)。

    由DOM Diff算法理解組件的生命周期

    上一篇文章中介紹了React組件的生命周期,其中的每個(gè)階段其實(shí)都是和DOM Diff算法息息相關(guān)的。例如以下幾個(gè)方法:

    • constructor: 構(gòu)造函數(shù),組件被創(chuàng)建時(shí)執(zhí)行;

    • componentDidMount: 當(dāng)組件添加到DOM樹之后執(zhí)行;

    • componentWillUnmount: 當(dāng)組件從DOM樹中移除之后執(zhí)行,在React中可以認(rèn)為組件被銷毀;

    • componentDidUpdate: 當(dāng)組件更新時(shí)執(zhí)行。

    為了演示組件生命周期和DOM Diff算法的關(guān)系,筆者創(chuàng)建了一個(gè)示例:https://supnate.github.io/react-dom-diff/index.html?,大家可以直接訪問試用。這時(shí)當(dāng)DOM樹進(jìn)行如下轉(zhuǎn)變時(shí),即從“shape1”轉(zhuǎn)變到“shape2”時(shí)。我們來觀察這幾個(gè)方法的執(zhí)行情況:

    瀏覽器開發(fā)工具控制臺輸出如下結(jié)果:


    C?will?unmount.
    C?is?created.
    B?is?updated.
    A?is?updated.
    C?did?mount.
    D?is?updated.
    R?is?updated.


    可以看到,C節(jié)點(diǎn)是完全重建后再添加到D節(jié)點(diǎn)之下,而不是將其“移動”過去。如果大家有興趣,也可以fork示例代碼:https://github.com/supnate/react-dom-diff?。從而可以自己添加其它樹結(jié)構(gòu),試驗(yàn)它們之間是如何轉(zhuǎn)換的。

    相同類型節(jié)點(diǎn)的比較

    第二種節(jié)點(diǎn)的比較是相同類型的節(jié)點(diǎn),算法就相對簡單而容易理解。React會對屬性進(jìn)行重設(shè)從而實(shí)現(xiàn)節(jié)點(diǎn)的轉(zhuǎn)換。例如:


    renderA:?<div?id="before"?/>
    renderB:?<div?id="after"?/>
    =>?[replaceAttribute?id?"after"]


    虛擬DOM的style屬性稍有不同,其值并不是一個(gè)簡單字符串而必須為一個(gè)對象,因此轉(zhuǎn)換過程如下:


    renderA:?<div?style={{color:?'red'}}?/>
    renderB:?<div?style={{fontWeight:?'bold'}}?/>
    =>?[removeStyle?color],?[addStyle?font-weight?'bold']


    列表節(jié)點(diǎn)的比較

    上面介紹了對于不在同一層的節(jié)點(diǎn)的比較,即使它們完全一樣,也會銷毀并重新創(chuàng)建。那么當(dāng)它們在同一層時(shí),又是如何處理的呢?這就涉及到列表節(jié)點(diǎn)的Diff算法。相信很多使用React的同學(xué)大多遇到過這樣的警告:

    這是React在遇到列表時(shí)卻又找不到key時(shí)提示的警告。雖然無視這條警告大部分界面也會正確工作,但這通常意味著潛在的性能問題。因?yàn)镽eact覺得自己可能無法高效的去更新這個(gè)列表。

    列表節(jié)點(diǎn)的操作通常包括添加、刪除和排序。例如下圖,我們需要往B和C直接插入節(jié)點(diǎn)F,在jQuery中我們可能會直接使用$(B).after(F)來實(shí)現(xiàn)。而在React中,我們只會告訴React新的界面應(yīng)該是A-B-F-C-D-E,由Diff算法完成更新界面。

    這時(shí)如果每個(gè)節(jié)點(diǎn)都沒有唯一的標(biāo)識,React無法識別每一個(gè)節(jié)點(diǎn),那么更新過程會很低效,即,將C更新成F,D更新成C,E更新成D,最后再插入一個(gè)E節(jié)點(diǎn)。效果如下圖所示:

    可以看到,React會逐個(gè)對節(jié)點(diǎn)進(jìn)行更新,轉(zhuǎn)換到目標(biāo)節(jié)點(diǎn)。而最后插入新的節(jié)點(diǎn)E,涉及到的DOM操作非常多。而如果給每個(gè)節(jié)點(diǎn)唯一的標(biāo)識(key),那么React能夠找到正確的位置去插入新的節(jié)點(diǎn),入下圖所示:

    對于列表節(jié)點(diǎn)順序的調(diào)整其實(shí)也類似于插入或刪除,下面結(jié)合示例代碼我們看下其轉(zhuǎn)換的過程。仍然使用前面提到的示例:https://supnate.github.io/react-dom-diff/index.html?,我們將樹的形態(tài)從shape5轉(zhuǎn)換到shape6:

    即將同一層的節(jié)點(diǎn)位置進(jìn)行調(diào)整。如果未提供key,那么React認(rèn)為B和C之后的對應(yīng)位置組件類型不同,因此完全刪除后重建,控制臺輸出如下:


    B?will?unmount.
    C?will?unmount.
    C?is?created.
    B?is?created.
    C?did?mount.
    B?did?mount.
    A?is?updated.
    R?is?updated.


    而如果提供了key,如下面的代碼:


    shape5:?function()?{
    ??return?(????<Root>
    ??????<A>
    ????????<B?key="B"?/>
    ????????<C?key="C"?/>
    ??????</A>
    ????</Root>
    ??);
    },

    shape6:?function()?{
    ??return?(????<Root>
    ??????<A>
    ????????<C?key="C"?/>
    ????????<B?key="B"?/>
    ??????</A>
    ????</Root>
    ??);
    },


    那么控制臺輸出如下:


    C?is?updated.
    B?is?updated.
    A?is?updated.
    R?is?updated.


    可以看到,對于列表節(jié)點(diǎn)提供唯一的key屬性可以幫助React定位到正確的節(jié)點(diǎn)進(jìn)行比較,從而大幅減少DOM操作次數(shù),提高了性能。

    小結(jié)

    本文分析了React的DOM Diff算法究竟是如何工作的,其復(fù)雜度控制在了O(n),這讓我們考慮UI時(shí)可以完全基于狀態(tài)來每次render整個(gè)界面而無需擔(dān)心性能問題,簡化了UI開發(fā)的復(fù)雜度。而算法優(yōu)化的基礎(chǔ)是文章開頭提到的兩個(gè)假設(shè),以及React的UI基于組件這樣的一個(gè)機(jī)制。理解虛擬DOM Diff算法不僅能夠幫助我們理解組件的生命周期,而且也對我們實(shí)現(xiàn)自定義組件時(shí)如何進(jìn)一步優(yōu)化性能具有指導(dǎo)意義。


    本文摘自異步社區(qū),發(fā)表人:?xiangzhihong?,作品:《虛擬DOM Diff算法解析》,未經(jīng)授權(quán),禁止轉(zhuǎn)載。


    推薦閱讀

    2018年5月新書書單(文末福利)

    2018年4月新書書單

    異步圖書最全Python書單

    一份程序員必備的算法書單

    第一本Python神經(jīng)網(wǎng)絡(luò)編程圖書





    長按二維碼,可以關(guān)注我們喲

    每天與你分享IT好文。


    異步圖書”后臺回復(fù)“關(guān)注”,即可免費(fèi)獲得2000門在線視頻課程

    點(diǎn)擊查看原文,內(nèi)容


    閱讀原文


    轉(zhuǎn)載于:https://blog.51cto.com/13127751/2134222

    總結(jié)

    以上是生活随笔為你收集整理的虚拟DOM Diff算法解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。