virtualDom的DIFF算法關鍵過程整理

判斷對應節點是否有必要進行比較(sameVnode)

function sameVnode(oldVnode, vnode){
    return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
  • 若是值得比較會執行patchVnode(oldVnode, vnode)
  • 若是不值得比較,新節點直接把老節點整個替換了

打補丁(patchVnode)

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    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(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

節點的比較有5種狀況vue

  1. if (oldVnode === vnode),他們的引用一致,能夠認爲沒有變化。
  2. if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本節點的比較,須要修改,則會調用Node.textContent = vnode.text。
  3. if( oldCh && ch && oldCh !== ch ), 兩個節點都有子節點,並且它們不同,這樣咱們會調用updateChildren函數比較子節點,這是diff的核心
  4. else if (ch),只有新的節點有子節點,調用createEle(vnode),vnode.el已經引用了老的dom節點,createEle函數會在老dom節點上添加子節點。
  5. else if (oldCh),新節點沒有子節點,老節點有子節點,直接刪除老節點。

更新子節點(updateChildren)

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {   //對於vnode.key的比較,會把oldVnode = null
                oldStartVnode = oldCh[++oldStartIdx] 
            }else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx]
            }else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx]
            }else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
            }else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode)
                api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                oldStartVnode = oldCh[++oldStartIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode)
                api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
                oldEndVnode = oldCh[--oldEndIdx]
                newStartVnode = newCh[++newStartIdx]
            }else {
               // 使用key時的比較
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
                }
                idxInOld = oldKeyToIdx[newStartVnode.key]
                if (!idxInOld) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    newStartVnode = newCh[++newStartIdx]
                }
                else {
                    elmToMove = oldCh[idxInOld]
                    if (elmToMove.sel !== newStartVnode.sel) {
                        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    }else {
                        patchVnode(elmToMove, newStartVnode)
                        oldCh[idxInOld] = null
                        api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                    }
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
        if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
        }else if (newStartIdx > newEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
}

主要的思路大概就是,定義變量,分別記錄當前比較的新、舊節點中的首尾索引與節點(oldStartVnode、oldEndVnode、newStartVnode、newEndVnode)(後續稱爲比較區間)node

圖片來自:https://raw.githubusercontent.com/aooy/blog/master/images/issues-2/diff2.png

(圖片來自:https://github.com/aooy/blog/... )git

經過對 oldStartVnode、oldEndVnode、newStartVnode、newEndVnode 作,兩兩的 sameVnode 比較github

比較判斷有4種,按順序依次比較
oldStartVnode —— newStartVnode (頭對頭)
oldEndVnode —— newEndVnode (尾對尾)
oldStartVnode —— newEndVnode (頭對尾)
oldEndVnode —— newStartVnode (尾對頭)

若是其中一種比較成立了,那麼oldVode中的相應節點會 以當前比較區間爲基準 移到newVnode相應的位置上,
而後比較區間會根據當前的比較條件類型,以頭或尾爲縮小比較區間的方向,縮小區間算法

例如: 當oldStartVnode,newEndVnode值得比較時, 將oldStartVnode.el移動到oldEndVnode.el後邊

圖片描述

當4種比較都不成立時,會使用key去比較,並在最終都使newVode的比較區間,頭部 減1api

當oldVnode的key列表中能匹配到對應的key時,判斷比較節點的選擇器屬性是否同樣dom

  • 不同則直接在當前比較區間的頭部,新建立一個newVnode的Dom插入函數

    比較節點中的oldVnode無需處理,由於後面的比較中不會有成立的比較條件,最終會直接刪除節點
  • 若是同樣則將比較節點中的oldVnode移動到當前比較區間的頭部(因此爲節點設置key能夠更高效的利用dom),並將比較區間中oldVnode本來的索引位置賦值爲Null

若是最終key列表也沒能匹配到的話,也是直接在當前比較區間的頭部,新建立一個newVnode的Dom插入。spa


最後在結束時會存在2種狀況code

  • oldStartIdx > oldEndIdx

    oldVnode先遍歷完,證實newVode有新增的節點,或者一致,這種狀況會將newVode剩餘的節點插入到oldVnode比較區間的末尾
  • newStartIdx > newEndIdx

    這時是newVode先遍歷完,證實newVode裏刪除了某些節點,此時oldVnode的比較區間節點是已經不存在的,會將他們刪除。

參考文章: 解析vue2.0的diff算法
相關文章
相關標籤/搜索