Vue的diff算法簡析

       我們知道對於DOM的操作是很慢很耗費資源,拖累性能的,特別是涉及到重排迴流,重新渲染DOM樹是一個性能殺手,所以各大框架都在這個方面做了優化,angularjs是髒值檢測機制,而react首先使用了virtual DOM,vue也使用了這個機制。虛擬dom樹就是用js的對象來儲存和表示dom節點,當發生改變時,通過新舊的虛擬dom的對比(通過diff算法)來做局部的修整,避免整棵dom樹的重新渲染。

真實dom和虛擬dom的表示:

<div>
    <p>123</p>
</div>
var Vnode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};

接下來我們就一起來看一下diff算法是如何比較新舊兩棵樹的差異的。

首先在比較新舊節點時不會跨層級去比較,而是隻比較相同的層級,如下圖所示,因此算法複雜度可以大大減少。

接下來看一下patch打補丁的核心代碼:

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
    	patchVnode(oldVnode, vnode)
    } else {
    	const oEl = oldVnode.el
    	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必須相同
  )
}

首先進行兩個節點標籤名,key值,type等的比較,若是值得比較的節點,則繼續比較內層節點,若不相同則證明vnode完全不同,直接用vnode代替oldvnode,oldvnode設爲null等待回收。接下來看一下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)
    	}else if (oldCh){
            api.removeChildren(el)
    	}
    }
}

首先定義真實的dom爲el,如果新舊節點指向對象相同則直接返回,如果文本節點不一樣則將el的文本節點設置爲vnode的文本節點,如果都存在子節點而且不相等,就執行updateChildren函數比較子節點,如果vnode存在子節點而oldvnode不存在,則直接真實化子節點放到el裏,如果vnode不存在子節點而oldvnode存在子節點則移除el下的子節點。

接下來看一下關鍵的當兩者都存在子節點且都不一樣時的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)
    }
}
  • 將vnode的子節點vch和oldvnode的子節點oldch提取出來
  • oldch和vch各有兩個頭尾的變量startIdx和EndIdx,它們的2個變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設置了key,就會用key進行比較,在比較的過程中,變量會往中間靠,一旦startIdx>EndIdx表明oldch和vch至少有一個已經遍歷完了,就會結束比較。

流程圖如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章