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

原文连接

相关文章
相关标签/搜索