溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue源碼分析之虛擬DOM的示例分析

發(fā)布時間:2021-05-31 09:43:46 來源:億速云 閱讀:143 作者:小新 欄目:開發(fā)技術(shù)

小編給大家分享一下Vue源碼分析之虛擬DOM的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

為什么需要虛擬dom?

虛擬DOM就是為了解決瀏覽器性能問題而被設(shè)計出來的。例如,若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內(nèi)容保存到本地一個JS對象中,最終將這個JS對象一次性attch到DOM樹上,再進(jìn)行后續(xù)操作,避免大量無謂的計算量。簡單來說,可以把Virtual DOM 理解為一個簡單的JS對象,并且最少包含標(biāo)簽名( tag)、屬性(attrs)和子元素對象( children)三個屬性。

  • ----- 元素節(jié)點: 元素節(jié)點更貼近于我們通常所看到的真實DOM節(jié)點,他有描述節(jié)點標(biāo)簽名詞的tag屬性,描述節(jié)點屬性如class,attributes等的data屬性,有描述包含的子節(jié)點信息的children屬性等,由于元素節(jié)點所包含的情況相對而言比較復(fù)雜,源碼中沒有像前三種節(jié)點一樣直接寫死。

  • VNode的作用: 用js的計算性能來換取操作真實DOM所消耗的性能,

  • ----- VNode在Vue的整個虛擬DOM過程起到了什么作用呢。 其實VNode的作用是相當(dāng)大的,我們在視圖渲染之前,把寫好的template模板先編譯成VNode并緩存下來,等到數(shù)據(jù)變化頁面需要重新渲染的時候,我們把數(shù)據(jù)發(fā)生變化后的生成的VNode與前一次緩存下來的VNode進(jìn)行對比,找出差異。然后有差異的VNode對應(yīng)的真實的DOM節(jié)點就是需要重新渲染的節(jié)點,最后根據(jù)有差異的創(chuàng)建出來的DOM節(jié)點再插入到視圖中,最終完成一次視圖更新。就是再數(shù)據(jù)變化前后生成真實的DOM對應(yīng)的虛擬DOM節(jié)點

為什么要有虛擬DOM:

----- 就是以JS的計算性能來換取操作真實DOM所消耗的性能,Vue是通過VNode類來實例化不同類型的虛擬DOM節(jié)點,并且學(xué)習(xí)了不同類型節(jié)點生成的屬性的不同,所謂不同類型的節(jié)點其本質(zhì)還是一樣的,都屬VNode類的實例,只是實例化的時候傳入的參數(shù)不同罷了。
有了數(shù)據(jù)變化前后的VNode,我們才能進(jìn)行后續(xù)的DOM-Diff找出差異,最終做到只更新有差異的視圖,從而達(dá)到盡可能少的操作真實DOM的目的,以節(jié)省性能

----- 而找出更新有差異的DOM節(jié)點,已達(dá)到最少操作真實DOM更新視圖的目的。而對比新舊兩份VNode并找出差異的過程就是所謂的DOM-Diff過程,DOM-Diff算法是整個虛擬DOM的核心所在。

Patch

在Vue中,把DOM-Diff過程就叫做patch過程,patch意思為補丁,一個思想:所謂舊的VNode(odlNode)就是數(shù)據(jù)變化之前屬于所對應(yīng)的虛擬DOM節(jié)點,而新的NVode是數(shù)據(jù)變化之后將要渲染的視圖所對應(yīng)的虛擬DOM節(jié)點,所以我們要以生成的新的VNode為基準(zhǔn),對比舊的oldVNode,如果新的VNode上有的節(jié)點而舊的oldVNode沒有,那么就在舊的oldVNode上加上去,如果新的VNode上沒有的節(jié)點而舊的oldVNode上有,那么就在舊的oldVnode上去掉。如果新舊Vnode節(jié)點都有,則以新的VNode為準(zhǔn),更新舊的oldVNode,從而讓新舊VNode相同。

整個patch:就是在創(chuàng)建節(jié)點:新的VNode有,舊的沒有。就在舊的oldVNode中創(chuàng)建

刪除節(jié)點:新的VNode中沒有,而舊的oldVNode有,就從舊的oldVNode中刪除

更新節(jié)點:新的舊的都有,就以新的VNode為準(zhǔn),更新舊的oldVNode

更新子節(jié)點

/* 
    對比兩個子節(jié)點數(shù)組肯定是要通過循環(huán),外層循環(huán)newChildren,內(nèi)層循環(huán)oldCHildren數(shù)組,每循環(huán)外層
    newChildren數(shù)組里的每一個子節(jié)點,就去內(nèi)層oldChildren數(shù)組里找看有沒有與之相同的子節(jié)點
*/
for (let i = 0; i < newChildred.length; i++) {
    const newChild = newChildren[i]
    for (let j = 0; j < oldChildren.length; j++) {
        const oldChild = oldChildren[i]
        if (newChild === oldChild) {
            // ...
        }
    }
}

那么以上這個過程將會存在一下四種情況

  1. 創(chuàng)建子節(jié)點,如果newChildren里面的某個子節(jié)點在oldChildren里找不到與之相同的子節(jié)點,那么說明newChildren里面的這個子節(jié)點是之前沒有的,是需要此次新增的節(jié)點,那就創(chuàng)建子節(jié)點

  2. 刪除子節(jié)點,如果把newChildren里面的每一個子節(jié)點都循環(huán)完畢后,oldChildren還有未處理的子節(jié)點,那就說明未處理的子節(jié)點式需要被廢棄的,那就把這些節(jié)點刪除

  3. 移動子節(jié)點,如果newChildren里面的某個子節(jié)點在oldChildren里找到了與之相同的子節(jié)點,但是所處的位置不同,這說明此次變化的需要調(diào)整該子節(jié)點的位置,那以newChildren里的子節(jié)點1的位置為基準(zhǔn),調(diào)整oldChildren里該節(jié)點的位置,使之與在newChildren里的位置相同

  4. 更新節(jié)點:如果newChildren里面的某個子節(jié)點在oldCHildren里找到了與之相同的子節(jié)點,并且所處的位置也相同,那么就更新oldChildren里該節(jié)點,使之與newChildren里的該節(jié)點相同

我們一再強調(diào)更新節(jié)點要以新Vnode為基準(zhǔn),然后操作舊的oldVnode,使之最后舊的oldVNode與新的VNode相同。

更新的時候分為三個部分:

如果VNode和oldVNode均為靜態(tài)節(jié)點,

我們說了,靜態(tài)節(jié)點無論數(shù)據(jù)發(fā)生任何變化都與它無關(guān),所以都為靜態(tài)節(jié)點的話則直接跳過,無需處理

如果VNode是文本節(jié)點

如果VNode是我文本節(jié)點即表示這個節(jié)點內(nèi)只包含純文本,那么只需要看oldVNode是否也是文本節(jié)點,如果是那就比較兩個文本是否不同,如果不用則把oldVNode里的文本改成跟VNode的文本一樣,如果oldVNode不是文本節(jié)點,那么不論它是什么,直接調(diào)用setTextNode方法把他改成文本節(jié)點,并且文本內(nèi)容跟VNode相同

如果VNode是元素節(jié)點,則又細(xì)分以下兩種情況

  1. 該節(jié)點包含子節(jié)點,那么此時要看舊的節(jié)點是否包含子節(jié)點,如果舊的節(jié)點里包含了子節(jié)點,那就需要遞歸對比更新子節(jié)點

  2. 如果舊的節(jié)點里不包含子節(jié)點,那么這個舊節(jié)點可能是空節(jié)點或者文本節(jié)點

  3. 如果舊的節(jié)點是空節(jié)點就把新的節(jié)點里的子節(jié)點創(chuàng)建一份然后插入到舊的節(jié)點里面,

  4. 如果舊的節(jié)點是文本節(jié)點,則把文本清空,然后把新的節(jié)點里的子節(jié)點創(chuàng)建一份然后插入到舊的節(jié)點里面

  5. 該節(jié)點不包含子節(jié)點,如果該節(jié)點不包含子節(jié)點,同時他又不是文本節(jié)點,那就說明該節(jié)點是個空節(jié)點,那就好辦了,不管舊的節(jié)點之前里面有啥,直接清空即可

// 更新節(jié)點
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // vnode 與 oldVnode 是否完全一樣,如果是,退出程序
    if (oldVnode === vnode) {
        return
    }
    const elm = vnode.elm = oldVnode.elm
    // vnode 與 oldVnode是否都是靜態(tài)節(jié)點,如果是退出程序
    if (isTrue(vnode.isStatic) && isTrue(vnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
        return
    }
    const oldCh = oldVnode.children
    const ch = vnode.children
    // vnode 有 text屬性,若沒有
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            // 若都存在,判斷子節(jié)點是否相同,不同則更新子節(jié)點
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        }
        // 若只有vnode的子節(jié)點的存在
        else if (isDef(ch)) {
            /**
             * 判斷oldVnode是否有文本
             * 若沒有,則把Vnode的子節(jié)點添加到真實DOM中
             * 若有,則清空DOM中的文本,再把vnode的子節(jié)點添加到真實DOM中
             *  */
            if (isDef(oldVnode.text)) nodeOps.setTextContext(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        }
        // 如果只有oldnode的子節(jié)點存在
        else if (isDef(oldCh)) {
            // 清空DOM中的所有子節(jié)點
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }
        // 若vnode和oldnode都沒有子節(jié)點,但是oldnode中有文本
        else if (isDef(oldVnode.text)) {
            nodeOps.setTextContext(elm, '')
        }
        // 上面兩個判斷一句話概括就是,如果vnode中既沒有text,也沒有子節(jié)點,那么對應(yīng)的oldnode中有什么清空什么
    } else if (oldVnode.text !== vnode.text) {
        nodeOps.setTextContext(elm, vnode.text)
    }
}

上面的我們了解了Vue的patch也就是DOM-DIFF算法,并且知道了在patch過程之中基本會干三件事,分別是創(chuàng)建節(jié)點,刪除節(jié)點和更新節(jié)點。 創(chuàng)建節(jié)點和刪除節(jié)點比較簡單,而更新節(jié)點因為要處理各種可能出現(xiàn)的情況邏輯就比較復(fù)雜一些。 更新過程中九點Vnode可能都包含子節(jié)點,對于子系欸但的對比更新會有額外的一些邏輯,那么本篇文章就來學(xué)習(xí)Vue中是如何對比子節(jié)點的

更新子節(jié)點

當(dāng)新的Vnode與舊的oldVnode都是元素節(jié)點并且都包含子節(jié)點的時候,那么這連個節(jié)點VNode實例上的chidlren屬性就是所包含的子節(jié)點數(shù)組,對比兩個子節(jié)點的通過循環(huán),外層循環(huán)newChildren數(shù)組,內(nèi)層循環(huán)oldChildren數(shù)組,每循環(huán)外層newChildren數(shù)組里的一個子節(jié)點,,就去內(nèi)層oldChiildren數(shù)組里找看有沒有與之相同的子節(jié)點

. 創(chuàng)建子節(jié)點

創(chuàng)建子節(jié)點的位置應(yīng)該是在所有未處理節(jié)點之前,而并非所有已處理節(jié)點之后。 因為如果把子節(jié)點插入到已處理后面,如果后續(xù)還要插入新節(jié)點,那么新增子節(jié)點就亂了

. 移動子節(jié)點

所有未處理結(jié)點之前就是我們要移動的目的的位置

優(yōu)化更新子節(jié)點:

前面我們介紹了當(dāng)新的VNode與舊的oldVNode都是元素節(jié)點并且都包含了子節(jié)點的時候,vue對子節(jié)點是先外層循環(huán)newChildren數(shù)組,再內(nèi)層循環(huán)oldChildren數(shù)組,每循環(huán)外層newChildren數(shù)組里的一個子節(jié)點,就去內(nèi)層oldChildren數(shù)組里找看有沒有與之相同的子節(jié)點,最后根據(jù)不同的情況做出不同的操作。這種還存在可優(yōu)化的地方,比如當(dāng)包含子節(jié)點數(shù)量較多的時候,這樣循環(huán)算法的時間復(fù)雜度就會變得很大,不利于性能提升。

方法:

  1. 先把newChildren數(shù)組里的所有未處理子節(jié)點的第一個子節(jié)點和oldChildren數(shù)組里所有未處理子節(jié)點的第一個子節(jié)點做對,如果相同,那就直接進(jìn)入更新節(jié)點的操作;

  2. 如果不同,再把newChildren數(shù)組里所有未處理子節(jié)點的最后一個節(jié)點和oldChildren數(shù)組里所有未處理子節(jié)點的最后一個子節(jié)點做比對,如果相同,那就直接進(jìn)入更新節(jié)點的操作;

  3. 如果不同,再把newChildren數(shù)組里所有未處理子節(jié)點的最后一個子節(jié)點和oldChildren數(shù)組里所有未處理子節(jié)點的第一個子節(jié)點做比對,如果相同,那就直接進(jìn)入更新節(jié)點的操作,更新完后再將oldChildren數(shù)組里的該節(jié)點移動到newChildren數(shù)組里節(jié)點相同的位置;如果不同,

  4. 再把newChildren數(shù)組里所有未處理子節(jié)點的第一個子節(jié)點和oldChildren數(shù)組里所有未處理子節(jié)點的最后一個子節(jié)點做比對,如果相同,那就直接進(jìn)入更新節(jié)點的操作,更新后再將oldChildren數(shù)組里的該節(jié)點移動到與newChildren數(shù)組里節(jié)點相同的位置;

  5. 最后四種情況都試完如果還不同,那就按照之前循環(huán)的方式來查找節(jié)點。
    Vue為了避免雙重循環(huán)數(shù)據(jù)量大時間復(fù)雜度升高帶來的性能問題,而選擇了從子節(jié)點數(shù)組中的四個特殊位置互相對比,分別是:新前和舊前,新后和舊后,新后和舊前,新前和舊后

在前面幾篇文章中,介紹了Vue中的虛擬DOM以及虛擬DOM的patch(DOM-Diff)過程,而虛擬DOM存在的必要條件是的現(xiàn)有VNode,那么VNode又是從哪里來的。 把用戶寫的模板進(jìn)行編譯,就會產(chǎn)生VNode

模板編譯:

什么是模板編譯:把用戶template標(biāo)簽里面的寫的類似于原生HTML的內(nèi)容進(jìn)行編譯,把原生HTML的內(nèi)容找出來,再把非原生的HTML找出來,經(jīng)過一系列的邏輯處理生成渲染函數(shù),也就是render函數(shù)的這一段過程稱之為模板編譯過程。 render函數(shù)會將模板內(nèi)容生成VNode

整體渲染流程,所謂渲染流程,就是把用戶寫的類似于原生HTML的模板經(jīng)過一系列的過程最終反映到視圖中稱之為整個渲染流程,這個流程在上文中已經(jīng)說到了。

抽象語法樹AST:

  • 用戶在template標(biāo)簽中寫的模板對Vue來說就是一堆字符串,那么如何解析這一堆字符串并且從中提取出來元素的標(biāo)簽,屬性,變量插值 等有效信息呢,這就需要借助一個叫做抽象語法樹的東西。
    抽象語法樹簡稱語法樹,是源代碼語法結(jié)構(gòu)的一種抽象表示,他以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu),之所以說語法是抽象的,是因為這里的語法并不會表示出真實語法中出現(xiàn)的每個析姐,比如,嵌套括號被隱含在樹的結(jié)構(gòu)中,并沒有以節(jié)點的i形式呈現(xiàn),

具體流程:

  • 將一堆字符串模板解析成抽象語法樹AST后,我們就可以對其進(jìn)行各種操作處理了,處理完畢之后的AST來生成render函數(shù),其具體三個流程可以分為以下三個階段

模板解析階段:將一堆模板字符串用正則表達(dá)式解析成抽象語法樹AST

優(yōu)化階段:編譯AST,找出其中的靜態(tài)節(jié)點,并打上標(biāo)記

代碼生成階段: 將AST轉(zhuǎn)換成渲染函數(shù)

有了模板編譯,才有了虛擬DOM,才有了后續(xù)的視圖更新

以上是“Vue源碼分析之虛擬DOM的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI