帶你搞懂Vue虛擬Dom和diff算法

前言

使用過Vue和React的小夥伴確定對虛擬Dom和diff算法很熟悉,它扮演着很重要的角色。因爲小編接觸Vue比較多,React只是淺學,因此本篇主要針對Vue來展開介紹,帶你一步一步搞懂它。javascript

虛擬DOM

什麼是虛擬DOM?

虛擬DOM(Virtual Dom),也就是咱們常說的虛擬節點,是用JS對象來模擬真實DOM中的節點,該對象包含了真實DOM的結構及其屬性,用於對比虛擬DOM和真實DOM的差別,從而進行局部渲染來達到優化性能的目的。html

真實的元素節點:vue

<div id="wrap">
    <p class="title">Hello world!</p>
</div>
複製代碼

VNode:java

{
    tag:'div',
    attrs:{
        id:'wrap'
    },
    children:[
        {
            tag:'p',
            text:'Hello world!',
            attrs:{
                class:'title',
            }
        }
    ]
}
複製代碼

爲何使用虛擬DOM?

簡單瞭解虛擬DOM後,是否是有小夥伴會問:Vue和React框架中爲何會用到它呢?好問題!那來解決下小夥伴的疑問。node

起初咱們在使用JS/JQuery時,不可避免的會大量操做DOM,而DOM的變化又會引起迴流或重繪,從而下降頁面渲染性能。那麼怎樣來減小對DOM的操做呢?此時虛擬DOM應用而生,因此虛擬DOM出現的主要目的就是爲了減小頻繁操做DOM而引發迴流重繪所引起的性能問題的算法

虛擬DOM的做用是什麼?

  1. 兼容性好。由於Vnode本質是JS對象,因此無論Node仍是瀏覽器環境,均可以操做;
  2. 減小了對Dom的操做。頁面中的數據和狀態變化,都經過Vnode對比,只須要在比對完以後更新DOM,不須要頻繁操做,提升了頁面性能;

虛擬DOM和真實DOM的區別?

說到這裏,那麼虛擬DOM和真實DOM的區別是什麼呢?總結大概以下:api

  • 虛擬DOM不會進行迴流和重繪;
  • 真實DOM在頻繁操做時引起的迴流重繪致使性能很低;
  • 虛擬DOM頻繁修改,而後一次性對比差別並修改真實DOM,最後進行依次迴流重繪,減小了真實DOM中屢次迴流重繪引發的性能損耗;
  • 虛擬DOM有效下降大面積的重繪與排版,由於是和真實DOM對比,更新差別部分,因此只渲染局部;
總損耗 = 真實DOM增刪改 + (多節點)迴流/重繪;    //計算使用真實DOM的損耗
總損耗 = 虛擬DOM增刪改 + (diff對比)真實DOM差別化增刪改 + (較少節點)迴流/重繪;   //計算使用虛擬DOM的損耗
複製代碼

能夠發現,都是圍繞頻繁操做真實DOM引發迴流重繪,致使頁面性能損耗來講的。不過框架也不必定非要使用虛擬DOM,關鍵在於看是否頻繁操做會引發大面積的DOM操做。數組

那麼虛擬DOM究竟經過什麼方式來減小了頁面中頻繁操做DOM呢?這就不得不去了解DOM Diff算法了。瀏覽器

DIFF算法

當數據變化時,vue如何來更新視圖的?其實很簡單,一開始會根據真實DOM生成虛擬DOM,當虛擬DOM某個節點的數據改變後會生成一個新的Vnode,而後VNode和oldVnode對比,把不一樣的地方修改在真實DOM上,最後再使得oldVnode的值爲Vnode。markdown

diff過程就是調用patch函數,比較新老節點,一邊比較一邊給真實DOM打補丁(patch);

對照vue源碼來解析一下,貼出核心代碼,旨在簡單明瞭講述清楚,否則小編本身看着都頭大了O(∩_∩)O

patch

那麼patch是怎樣打補丁的?

//patch函數 oldVnode:老節點 vnode:新節點
function patch (oldVnode, vnode) {
    ...
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)    //若是新老節點是同一節點,那麼進一步經過patchVnode來比較子節點
    } else {
        /* -----不然新節點直接替換老節點----- */
        const oEl = oldVnode.el // 當前oldVnode對應的真實元素節點
        let parentEle = api.parentNode(oEl)  // 父元素
        createEle(vnode)  // 根據Vnode生成新元素
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 將新元素添加進父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除之前的舊元素節點
            oldVnode = null
        }
    }
    ...
    return vnode
}

//判斷兩節點是否爲同一節點
function sameVnode (a, b) {
    return (
	    a.key === b.key &&  // key值
	    a.tag === b.tag &&  // 標籤名
	    a.isComment === b.isComment &&  // 是否爲註釋節點
	    // 是否都定義了data,data包含一些具體信息,例如onclick , style
	    isDef(a.data) === isDef(b.data) &&  
	    sameInputType(a, b) // 當標籤是<input>的時候,type必須相同
    )
}
複製代碼

從上面能夠看出,patch函數是經過判斷新老節點是否爲同一節點:

  • 若是是同一節點,執行patchVnode進行子節點比較;

  • 若是不是同一節點,新節點直接替換老節點;

那若是不是同一節點,可是它們子節點同樣怎麼辦嘞?OMG,要牢記:diff是同層比較,不存在跨級比較的!簡單提一嘴,React中也是如此,它們只是針對同一層的節點進行比較。

patchVnode

既然到了patchVnode方法,說明新老節點爲同一節點,那麼這個方法作了什麼處理?

function patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el           //找到對應的真實DOM
    let i, oldCh = oldVnode.children, ch = vnode.children     
    if (oldVnode === vnode) return         //若是新老節點相同,直接返回
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        //若是新老節點都有文本節點且不相等,那麼新節點的文本節點替換老節點的文本節點
        api.setTextContent(el, vnode.text)  
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            //若是新老節點都有子節點,執行updateChildren比較子節點[很重要也很複雜,下面展開介紹]
            updateChildren(el, oldCh, ch)
        }else if (ch){
            //若是新節點有子節點而老節點沒有子節點,那麼將新節點的子節點添加到老節點上
            createEle(vnode)
        }else if (oldCh){
            //若是新節點沒有子節點而老節點有子節點,那麼刪除老節點的子節點
            api.removeChildren(el)
        }
    }
}
複製代碼

若是兩個節點不同,直接用新節點替換老節點;

若是兩個節點同樣,

  • ​ 新老節點同樣,直接返回;
  • ​ 老節點有子節點,新節點沒有:刪除老節點的子節點;
  • ​ 老節點沒有子節點,新節點有子節點:新節點的子節點直接append到老節點;
  • ​ 都只有文本節點:直接用新節點的文本節點替換老的文本節點;
  • ​ 都有子節點:updateChildren

最複雜的狀況也就是新老節點都有子節點,那麼updateChildren是如何來處理這一問題的,該方法也是diff算法的核心,下面咱們來了解一下!

updateChildren

因爲代碼太多了,這裏先作個概述。updateChildren方法的核心:

  1. 提取出新老節點的子節點:新節點子節點ch和老節點子節點oldCh;
  2. ch和oldCh分別設置StartIdx(指向頭)和EndIdx(指向尾)變量,它們兩兩比較(按照sameNode方法),有四種方式來比較。若是4種方式都沒有匹配成功,若是設置了key就經過key進行比較,在比較過程種startIdx++,endIdx--,一旦StartIdx > EndIdx代表ch或者oldCh至少有一個已經遍歷完成,此時就會結束比較。

下面結合圖來理解:

第一步:

oldStartIdx = A , oldEndIdx = C;
newStartIdx = A , newEndIdx = D;
複製代碼

此時oldStartIdx和newStarIdx匹配,因此將dom中的A節點放到第一個位置,此時A已經在第一個位置,因此不作處理,此時真實DOM順序:A B C;

第二步:

oldStartIdx = B , oldEndIdx = C;
newStartIdx = C , oldEndIdx = D;
複製代碼

此時oldEndIdx和newStartIdx匹配,將本來的C節點移動到A後面,此時真實DOM順序:A C B;

第三步:

oldStartIdx = C , oldEndIdx = C;
newStartIdx = B , newEndIdx = D;
oldStartIdx++,oldEndIdx--;
oldStartIdx > oldEndIdx
複製代碼

此時遍歷結束,oldCh已經遍歷完,那麼將剩餘的ch節點根據本身的index插入到真實DOM中便可,此時真實DOM順序:A C B D;

因此匹配過程當中判斷結束有兩個條件:

  • oldStartIdx > oldEndIdx表示oldCh先遍歷完成,若是ch有剩餘節點就根據對應index添加到真實DOM中;
  • newStartIdx > newEndIdx表示ch先遍歷完成,那麼就要在真實DOM中將多餘節點刪除掉;

看下圖這個實例,就是新節點先遍歷完成刪除多餘節點:

最後,在這些子節點sameVnode後若是知足條件繼續執行patchVnode,層層遞歸,直到oldVnode和Vnode中全部子節點都比對完成,也就把全部的補丁都打好了,此時更新到視圖。

總結

dom的diff算法時間複雜度爲o(n^3),若是使用在框架中性能會不好。Vue使用的diff算法,時間複雜度爲o(n),簡化了不少操做。

最後,用一張圖來記憶整個Diff過程,但願你能有所收穫!

彩蛋

由於React只是簡單學了基礎,這裏做爲對比來概述一下:

1.React渲染機制:React採用虛擬DOM,在每次屬性和狀態發生變化時,render函數會返回不一樣的元素樹,而後對比返回的元素樹和上次渲染樹的差別並對差別部分進行更新,最後渲染爲真實DOM。

2.diff永遠都是同層比較,若是節點類型不一樣,直接用新的替換舊的。若是節點類型相同,就比較他們的子節點,依次類推。一般元素上綁定的key值就是用來比較節點的,因此必定要保證其惟一性,通常不採用數組下標來做爲key值,由於當數組元素髮生變化時index會有所改動。

3.渲染機制的整個過程包含了更新操做,將虛擬DOM轉換爲真實DOM,因此整個渲染過程就是Reconciliation。而這個過程的核心又主要是diff算法,利用的是生命週期shouldComponentUpdate函數。

相關文章
相關標籤/搜索