Diff算法

Diff

  • diff對比的就是vnode
  • 同時因爲dom不多跨級移動,因此對比只在同層級中進行
  • vue和react的diff算法大致是同樣的

VNode

  • vue的vnode以下
{
    el:  div  //對真實的節點的引用,本例中就是document.querySelector('#id.classA')
    tagName: 'DIV',   //節點的標籤
    sel: 'div#v.classA'  //節點的選擇器
    data: null,       // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style
    children: [], //存儲子節點的數組,每一個子節點也是vnode結構
    text: null,    //若是是文本節點,對應文本節點的textContent,不然爲null
}
  • react的vnode以下
{
    type: 'div',
    props: {
        className: 'myDiv',
    },
    chidren: [
        { type: 'p', props: { value:'1' } },
        { type: 'div', props: { value:'2' } },
        { type: 'span', props: { value:'3' } }
    ]
}

Vue

  • vue的diff是邊比較邊更新的過程
  • 更改數據時,觸發試圖更新,會生成一棵新的vdom樹
  • 對比同級的vnode,僅當vue認爲兩個vnode值得比較時,纔會繼續對比其子節點,若是認爲不值得比較,會直接刪除舊節點,插入新節點
function patch (oldVnode, vnode) {
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode)
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
            api.removeChild(parentEle, oldVnode.el)
            oldVnode = null
        }
    }
    return vnode
}

function sameVnode(oldVnode, vnode){
  // 兩節點key值相同,而且sel屬性值相同,即認爲兩節點屬同一類型,可進行下一步比較
    return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
  • 針對同類型的節點,進行patch操做,並比較他的子節點
patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el  //讓vnode.el引用到如今的真實dom,當el修改時,vnode.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)
        //對於擁有子節點(二者的子節點不一樣)的兩個節點,調用updateChildren
        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)
        }
    }
}
  • 針對雙方都擁有子節點的狀況,經過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)
        }
}
  • 概況下,就是分別新建4個指針,指向新舊節點的頭尾,而後經過兩兩比對,看是不是同一個類型的節點
  • 而後分狀況,若是新舊頭節點是同一個類型節點,直接調用patchVnode便可,同理新舊尾節點是同一個類型節點,也是patchVnode處理
  • 但若是判斷新頭和舊尾是同一個類型節點,除了要patchVnode處理外,還須要將尾部節點移到頭部
  • 同理,若是新尾和舊頭是同一個類型節點,patchVnode處理後,將頭節點移到尾部
  • 若是四種比較都沒有擊中,這時判斷新頭的節點是否在仍未被對比的舊節點中,若是在,patchVnode處理,並在dom中將舊節點移到新頭的位置,本來舊節點對應的Vnode置空,這樣下次判斷就會被跳過,而若是新節點不在舊節點列表裏,則直接新建節點插入便可
  • 最後,當頭指針和尾指針相遇並錯事後,判斷當前四個指針的位置,若是舊的頭尾指針中間還有節點,那這些節點都是多餘的,須要被移除,若是新的頭尾指針中間還有節點,那這些節點都是新增的,須要被插入

React

  • react的diff是先比較,後更新的流程,對比階段會先記錄下差別
function diff (oldTree, newTree) {
  // 當前節點的標誌,之後每遍歷到一個節點,加1
  var index = 0
  var patches = {} // 用來記錄每一個節點差別的對象
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
  // 對比oldNode和newNode的不一樣,記錄下來,省略了代碼...
  patches[index] = [...]

  diffChildren(oldNode.children, newNode.children, index, patches)
}
  • 先對比當前節點,記錄差別,而後對比子節點
  • 子節點的對比直接從左節點開始,判斷其是否在舊節點列表中,若是不在,則記錄新建,若是在,則標記移動
  • 最後根據差別統一更新dom

區別

  • vue的diff是邊比較邊更新的,react則是先記錄差別,再統一更新
  • vue認爲兩個節點元素是同一個元素是根據key以及sel來判斷的,若是元素類型相同,但classname不一致的話也會認爲是不一樣的元素,而react是根據元素類型以及key來判斷的,因此會認爲是同一個元素類型,直接在這上面進行屬性增減
  • diff策略也不同,vue是新舊頭尾四個節點對比,react則是從左到右進行對比
  • 另外react16提出了fiber這個概念,diff是基於fiber樹進行的,且能夠被打斷,讓瀏覽器能處理更高優先級的任務,而vue的diff和patch是不能被打斷的

key的做用

  • vue和react的key做用是同樣的,就是告訴引擎當前對比的兩個節點是不是同一個節點
  • vue沒有key的時候,兩個都是undefined,則vue會認爲其是相同節點,而就地複用這個元素,但極可能這是個input元素,你已經在裏面輸入了某些信息,而更新時不會移動這些信息致使最終結果可能不是你所指望的,因此須要指明key來防止元素的複用
  • react則會默認賦值給key,因此子元素中,新舊節點的位置可能發生了移動,但由於react默認index做爲key,仍是會認爲相同位置上的元素是同一個元素,而在其基礎上修改屬性,致使發生和vue同樣的問題
相關文章
相關標籤/搜索