virtual-dom 梳理分析【diff 算法】

上一篇介紹了VD的是怎麼建立VD Tree的和怎麼根據VD Tree生成真實的DOM上一章連接node

這一章主要是來梳理當咱們的VD有變化的時候,它的diff算法是怎麼去比較生成一個diff對象的。react

Diff 算法是 VD 中最核心的一個算法。經過輸入初始狀態狀態A(VNode)和最終狀態B(VNode),經過計算,就能夠到獲得描述從A到B狀態的對象(VPatch),而後再根據這個描述對象,咱們就能知道哪些節點是須要新增的,哪些節點是須要刪除的,哪些節點只是屬性變化了須要更新的等等這些。git

根據github.com/Matt-Esch/v…的源碼來看,Diff 算法主要有三種狀況,分別是:github

  1. VNode diff,當前 VD 節點的比較。
  2. props diff,當前節點的屬性比較。
  3. child diff,對當前節點的子節點進行比較,其實就是遞歸調用 1和2 步驟。

當前節點的比較

如下文章,將前一個狀態稱爲A,變動後的狀態稱爲B。算法

function diff(a, b) {
    var patch = { a: a }
    walk(a, b, patch, 0)
    return patch
}
複製代碼

整個diff的算法的入口就是上面列的函數,首席聲明瞭一個patch對象,默認將前一個VD Tree存起來,整個 patch對象最終會被傳入到walk函數,進行加工最終獲得VPatch對象(描述各個節點的變化)。數組

function walk(a, b, patch, index) {
    if (a === b) {
        return
    }
    
    // 由於判斷子元素的時候,會遞歸調用這個函數,
    // 會嘗試的去獲取這個下標是否以前計算過。
    var apply = patch[index]
    var applyClear = false

    if (isThunk(a) || isThunk(b)) {
        thunks(a, b, patch, index)
    } else if (b == null) {
        if (!isWidget(a)) {
            clearState(a, patch, index)
            apply = patch[index]
        }
        apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
    } else if (isVNode(b)) {
        if (isVNode(a)) {
            if (a.tagName === b.tagName &&
                a.namespace === b.namespace &&
                a.key === b.key) {
                var propsPatch = diffProps(a.properties, b.properties)
                if (propsPatch) {
                    apply = appendPatch(apply,
                        new VPatch(VPatch.PROPS, a, propsPatch))
                }
                apply = diffChildren(a, b, patch, apply, index)
            } else {
                apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
                applyClear = true
            }
        } else {
            apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
            applyClear = true
        }
    } else if (isVText(b)) {
        if (!isVText(a)) {
            apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
            applyClear = true
        } else if (a.text !== b.text) {
            apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
        }
    } else if (isWidget(b)) {
        if (!isWidget(a)) {
            applyClear = true
        }
        apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
    }

    if (apply) {
        patch[index] = apply
    }

    if (applyClear) {
        clearState(a, patch, index)
    }
}
複製代碼

代碼還算比較長,可是邏輯仍是比較清楚,下面來對每一個分之進行分析。app

步驟分析

  1. 先比較AB若是是全等,那就是節點一點都沒有變動,直接結束。
if (a === b) {
    return
}
複製代碼
  1. 若是A或者B被判斷爲Thunk則使用Thunk的比較方式。這裏最終仍是會調用diff函數,回到節點的比較,中間會多幾層判斷。
if (isThunk(a) || isThunk(b)) {
   thunks(a, b, patch, index)
}
複製代碼
  1. 若是B爲空,就會生成一個標爲REMOVEVPatch對象。
else if (b == null) {
    // If a is a widget we will add a remove patch for it
    // Otherwise any child widgets/hooks must be destroyed.
    // This prevents adding two remove patches for a widget.
    if (!isWidget(a)) {
        clearState(a, patch, index)
        apply = patch[index]
    }
    apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
}
複製代碼
  1. 若是B是一個VD對象,接下來就開始進行比較:
    1. 若是 A 也是一個VD對象,經過比較tagNamenamespacekey
    2. 若是這三個都相同,則進一步去比較Props,獲得propsVPatch對象(這個放到Props diff分析),比較完props以後,繼續比較child子節點(放到後面講)
    3. 三個值其中有一個不一樣,將當前節點標記爲VNODE,也就是表示該節點標記爲替換
else if (isVNode(b)) {
    if (isVNode(a)) {
         if (a.tagName === b.tagName &&
             a.namespace === b.namespace &&
             a.key === b.key) {
             var propsPatch = diffProps(a.properties, b.properties)
             if (propsPatch) {
                 apply = appendPatch(apply,
                        new VPatch(VPatch.PROPS, a, propsPatch))
             }
             apply = diffChildren(a, b, patch, apply, index)
         } else {
             apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
             applyClear = true
         }
     } else {
         apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
         applyClear = true
     }
}
複製代碼
  1. 若是B是文本節點,A不是文本節點,那就標記當前節點爲VTEXT也就是將當前節點替換成文本節點。若是A也是文本節點,那就比較AB節點的值,若是不一樣則標記替換文本節點。
else if (isVText(b)) {
     if (!isVText(a)) {
        apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
        applyClear = true
     } else if (a.text !== b.text) {
        apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
    }
}
複製代碼

6.若是B節點是Widget,就將當前節點替換Widget元素,標記爲WIDGETdom

else if (isWidget(b)) {
    if (!isWidget(a)) {
        applyClear = true
    }
    apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
}
複製代碼
  1. 將上面判斷得出的結果賦值到patch[index] 中,apply就是對當前節點變更的描述對象了。
if (apply) {
    patch[index] = apply
}
複製代碼

上面7個步驟就是VNodediff算法,能夠看到,在BVNode的狀況下,還會去繼續比較BA的屬性和子元素。函數

props 的比較

props的diff算法,文件地址,能夠看到,整個函數是一個for循環,使用for in循環來遍歷A的屬性。源碼分析

function diffProps(a, b) {
    var diff

    for (var aKey in a) {
        if (!(aKey in b)) {
            diff = diff || {}
            diff[aKey] = undefined
        }

        var aValue = a[aKey]
        var bValue = b[aKey]

        if (aValue === bValue) {
            continue
        } else if (isObject(aValue) && isObject(bValue)) {
            if (getPrototype(bValue) !== getPrototype(aValue)) {
                diff = diff || {}
                diff[aKey] = bValue
            } else if (isHook(bValue)) {
                 diff = diff || {}
                 diff[aKey] = bValue
            } else {
                var objectDiff = diffProps(aValue, bValue)
                if (objectDiff) {
                    diff = diff || {}
                    diff[aKey] = objectDiff
                }
            }
        } else {
            diff = diff || {}
            diff[aKey] = bValue
        }
    }

    for (var bKey in b) {
        if (!(bKey in a)) {
            diff = diff || {}
            diff[bKey] = b[bKey]
        }
    }

    return diff
}
複製代碼
  1. 若是A元素裏面的屬性在B元素中已經不存在了,則將diff[aKey]置爲undefined,用來標記爲刪除。
if (!(aKey in b)) {
    diff = diff || {}
    diff[aKey] = undefined
}
複製代碼
  1. 獲取AB裏面相同Key的值,也就是當前遍歷的Key對應的值。
var aValue = a[aKey]
var bValue = b[aKey]
複製代碼
  1. 若是值是相等的接直接遍歷下一個Key
if (aValue === bValue) {
    continue
}
複製代碼
  1. 若是AB這兩個屬性都是對象,則繼續往下比較。
    1. 若是兩個對象的原型不相同,則記錄diff[aKey] = bValue
    2. 若是B的屬性是Hook,則記錄diff[aKey] = bValue
    3. 遞歸比較AB的當前屬性,這兩個對象,獲得的diffObject記錄到diff[aKey] = objectDiff。經過這點能夠看到這個庫的props的比較是深比較,會遞歸比較props的每個Key
else if (isObject(aValue) && isObject(bValue)) {
        if (getPrototype(bValue) !== getPrototype(aValue)) {
            diff = diff || {}
            diff[aKey] = bValue
        } else if (isHook(bValue)) {
             diff = diff || {}
             diff[aKey] = bValue
        } else {
            var objectDiff = diffProps(aValue, bValue)
            if (objectDiff) {
                diff = diff || {}
                diff[aKey] = objectDiff
            }
       }
}
複製代碼
  1. 若是當前兩個值不是對象且不相等,則標記diff[aKey] = bValue
else {
    diff = diff || {}
    diff[aKey] = bValue
}
複製代碼
  1. 遍歷B中有可是A總沒有的Key,也就是新增的Key,標記爲diff[bKey] = b[bKey]
for (var bKey in b) {
    if (!(bKey in a)) {
        diff = diff || {}
        diff[bKey] = b[bKey]
    }
}
複製代碼

最後函數放回當前的diff對象。

child 的比較

以前說過,childdiff 其實仍是會遞歸調用的 diff函數,下面咱們來看看。

function diffChildren(a, b, patch, apply, index) {
    var aChildren = a.children
    var orderedSet = reorder(aChildren, b.children)
    var bChildren = orderedSet.children

    var aLen = aChildren.length
    var bLen = bChildren.length
    var len = aLen > bLen ? aLen : bLen

    for (var i = 0; i < len; i++) {
        var leftNode = aChildren[i]
        var rightNode = bChildren[i]
        index += 1

        if (!leftNode) {
            if (rightNode) {
                // Excess nodes in b need to be added
                apply = appendPatch(apply,
                    new VPatch(VPatch.INSERT, null, rightNode))
            }
        } else {
            walk(leftNode, rightNode, patch, index)
        }

        if (isVNode(leftNode) && leftNode.count) {
            index += leftNode.count
        }
    }

    if (orderedSet.moves) {
        // Reorder nodes last
        apply = appendPatch(apply, new VPatch(
            VPatch.ORDER,
            a,
            orderedSet.moves
        ))
    }

    return apply
}
複製代碼
  1. ABchild放在一塊兒進行順序調整,方便以後能更好的比較。
var aChildren = a.children
var orderedSet = reorder(aChildren, b.children)
var bChildren = orderedSet.children
複製代碼
  1. 獲取兩個元素子節點的最大長度。
var aLen = aChildren.length
var bLen = bChildren.length
var len = aLen > bLen ? aLen : bLen
複製代碼
  1. 開始循環遍歷子節點。
for (var i = 0; i < len; i++) {
...
}
複製代碼
  1. 若是A節點的當前子節點是不存在的,可是B節點卻有。標記爲插入新節點。
if (!leftNode) {
    if (rightNode) {
        // Excess nodes in b need to be added
        apply = appendPatch(apply,
            new VPatch(VPatch.INSERT, null, rightNode))
    }
}
複製代碼
  1. 若是AB兩個節點的當前子節點都是存在的,則遞歸調用walk函數操做,注意這裏傳入的index爲當前子節點的下標,這就是walk函數中index的來源了,主要是用來區分子元素的。
else {
     walk(leftNode, rightNode, patch, index)
}
複製代碼
  1. 循環比較完節點後,來判斷以前的排序算法,若是隻是順序換了一下,則標記爲ORDER表示知識更換了順序。
if (orderedSet.moves) {
    // Reorder nodes last
    apply = appendPatch(apply, new VPatch(
        VPatch.ORDER,
        a,
        orderedSet.moves
    ))
}
複製代碼

reorder 函數分析

這個函數就是上面第一步中,進行調整順序的函數,裏面會使用到咱們常常看到React 中說 同級節點須要添加的 key

// List diff, naive left to right reordering
function reorder(aChildren, bChildren) {
    // O(M) time, O(M) memory
    var bChildIndex = keyIndex(bChildren)
    var bKeys = bChildIndex.keys
    var bFree = bChildIndex.free

    if (bFree.length === bChildren.length) {
        return {
            children: bChildren,
            moves: null
        }
    }

    // O(N) time, O(N) memory
    var aChildIndex = keyIndex(aChildren)
    var aKeys = aChildIndex.keys
    var aFree = aChildIndex.free

    if (aFree.length === aChildren.length) {
        return {
            children: bChildren,
            moves: null
        }
    }

    // O(MAX(N, M)) memory
    var newChildren = []

    var freeIndex = 0
    var freeCount = bFree.length
    var deletedItems = 0

    // Iterate through a and match a node in b
    // O(N) time,
    for (var i = 0 ; i < aChildren.length; i++) {
        var aItem = aChildren[i]
        var itemIndex

        if (aItem.key) {
            if (bKeys.hasOwnProperty(aItem.key)) {
                // Match up the old keys
                itemIndex = bKeys[aItem.key]
                newChildren.push(bChildren[itemIndex])

            } else {
                // Remove old keyed items
                itemIndex = i - deletedItems++
                newChildren.push(null)
            }
        } else {
            // Match the item in a with the next free item in b
            if (freeIndex < freeCount) {
                itemIndex = bFree[freeIndex++]
                newChildren.push(bChildren[itemIndex])
            } else {
                // There are no free items in b to match with
                // the free items in a, so the extra free nodes
                // are deleted.
                itemIndex = i - deletedItems++
                newChildren.push(null)
            }
        }
    }

    var lastFreeIndex = freeIndex >= bFree.length ?
        bChildren.length :
        bFree[freeIndex]

    // Iterate through b and append any new keys
    // O(M) time
    for (var j = 0; j < bChildren.length; j++) {
        var newItem = bChildren[j]

        if (newItem.key) {
            if (!aKeys.hasOwnProperty(newItem.key)) {
                // Add any new keyed items
                // We are adding new items to the end and then sorting them
                // in place. In future we should insert new items in place.
                newChildren.push(newItem)
            }
        } else if (j >= lastFreeIndex) {
            // Add any leftover non-keyed items
            newChildren.push(newItem)
        }
    }

    var simulate = newChildren.slice()
    var simulateIndex = 0
    var removes = []
    var inserts = []
    var simulateItem

    for (var k = 0; k < bChildren.length;) {
        var wantedItem = bChildren[k]
        simulateItem = simulate[simulateIndex]

        // remove items
        while (simulateItem === null && simulate.length) {
            removes.push(remove(simulate, simulateIndex, null))
            simulateItem = simulate[simulateIndex]
        }

        if (!simulateItem || simulateItem.key !== wantedItem.key) {
            // if we need a key in this position...
            if (wantedItem.key) {
                if (simulateItem && simulateItem.key) {
                    // if an insert doesn't put this key in place, it needs to move
                    if (bKeys[simulateItem.key] !== k + 1) {
                        removes.push(remove(simulate, simulateIndex, simulateItem.key))
                        simulateItem = simulate[simulateIndex]
                        // if the remove didn't put the wanted item in place, we need to insert it
                        if (!simulateItem || simulateItem.key !== wantedItem.key) {
                            inserts.push({key: wantedItem.key, to: k})
                        }
                        // items are matching, so skip ahead
                        else {
                            simulateIndex++
                        }
                    }
                    else {
                        inserts.push({key: wantedItem.key, to: k})
                    }
                }
                else {
                    inserts.push({key: wantedItem.key, to: k})
                }
                k++
            }
            // a key in simulate has no matching wanted key, remove it
            else if (simulateItem && simulateItem.key) {
                removes.push(remove(simulate, simulateIndex, simulateItem.key))
            }
        }
        else {
            simulateIndex++
            k++
        }
    }

    // remove all the remaining nodes from simulate
    while(simulateIndex < simulate.length) {
        simulateItem = simulate[simulateIndex]
        removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key))
    }

    // If the only moves we have are deletes then we can just
    // let the delete patch remove these items.
    if (removes.length === deletedItems && !inserts.length) {
        return {
            children: newChildren,
            moves: null
        }
    }

    return {
        children: newChildren,
        moves: {
            removes: removes,
            inserts: inserts
        }
    }
}
複製代碼

這個函數有點長,仍是一步一步來梳理。

  1. 根據keyIndex函數獲取bChildren設置了key和沒有設置key的元素下標集合。若是都沒有設置key就直接將bChildren返回。
var bChildIndex = keyIndex(bChildren)
var bKeys = bChildIndex.keys
var bFree = bChildIndex.free

if (bFree.length === bChildren.length) {
    return {
        children: bChildren,
        moves: null
    }
}
複製代碼
  1. 與第一步驟同樣, 根據keyIndex函數獲取aChildren設置了key和沒有設置key的元素下標集合。若是都沒有設置key就直接將bChildren返回。
var aChildIndex = keyIndex(aChildren)
var aKeys = aChildIndex.keys
var aFree = aChildIndex.free

if (aFree.length === aChildren.length) {
    return {
        children: bChildren,
        moves: null
    }
}
複製代碼
  1. 遍歷aChildren,分爲兩種狀況。
    1. aItem 存在key,則根據keybChildrenkeys集合中找,若是找的到,則將bChildren對應的節點 pushnewChildren 中。找不到則 push 一個 nullnewChildren
    2. aItem 不存在key,則去bChildren中沒有keys的集合中找第一個元素,將該元素 pushnewChildren 中,若是已經找完了或者爲空,則 push 一個 nullnewChildren
if (aItem.key) {
  if (bKeys.hasOwnProperty(aItem.key)) {
    // Match up the old keys
    itemIndex = bKeys[aItem.key]
    newChildren.push(bChildren[itemIndex])

  } else {
    // Remove old keyed items
    itemIndex = i - deletedItems++
    newChildren.push(null)
  }
} else {
  // Match the item in a with the next free item in b
  if (freeIndex < freeCount) {
    itemIndex = bFree[freeIndex++]
    newChildren.push(bChildren[itemIndex])
  } else {
    // There are no free items in b to match with
    // the free items in a, so the extra free nodes
    // are deleted.
    itemIndex = i - deletedItems++
    newChildren.push(null)
  }
}
複製代碼
  1. 遍歷 bChildren ,將對應在 aChildren 中沒有的 key 對應的元素或者尚未被添加到 newChildren 的剩下元素 pushnewChildren
for (var j = 0; j < bChildren.length; j++) {
  var newItem = bChildren[j]
  if (newItem.key) {
    if (!aKeys.hasOwnProperty(newItem.key)) {
      // Add any new keyed items
      // We are adding new items to the end and then sorting them
      // in place. In future we should insert new items in place.
      newChildren.push(newItem)
    }
  } else if (j >= lastFreeIndex) {
    // Add any leftover non-keyed items
    newChildren.push(newItem)
  }
}
複製代碼
  1. 通過3和4步驟,就應該獲得 newChildren 數組了,最後將bChildren和新數組逐個比較,獲得重新數組轉換到bChildren數組的move操做patch(即remove+insert)。
for (var k = 0; k < bChildren.length;) {
...
  if (!simulateItem || simulateItem.key !== wantedItem.key) {
    ... 
      ...
      removes.push(remove(simulate, simulateIndex, simulateItem.key))
      simulateItem = simulate[simulateIndex]
      ...
   ...
  }
}
複製代碼
  1. 最後返回調整後的 chilren 數組和相關標記,新數組和move操做列表。這就能夠看到 diffChilren 裏面有 if (orderedSet.moves) 判斷,優化比較避免新增元素。更多能夠看React 源碼剖析系列 - 難以想象的 react diff

小結

經過上面三部分的分析,發現 diff 算法就是按照 DOM 的描述來進行比較的,在比較 children 的時候會利用 key 來優化標記,避免重複建立新 DOM。經過遞歸來比較 VD 獲得 VPatch 對象。

上一章說了 VD 樹的生成和 DOM 的建立,結合這章就能夠知道當數據和頁面變動後,VD 是怎麼去比較的,總體來講,梳理了一遍仍是明白了很多東西。

下一章就來梳理 patch 的過程了。

參考文章連接: 如何實現一個虛擬 DOM——virtual-dom 源碼分析 React 源碼剖析系列 - 難以想象的 react diff

原文連接

相關文章
相關標籤/搜索