简述 Diff 算法的执行过程

执行Diff算法的过程就是调用名为 patch 的函数,比较新旧节点。一边比较一边给真实的 DOM 打补丁。patch 函数接收两个参数 oldVnode 和 Vnode,它们分别代表新的节点和之前的旧节点。这个patch函数会比较 oldVnode 和 vnode 是否是相同的, 即函数 sameVnode(oldVnode, vnode), 根据这个函数的返回结果分如下两种情况:

  • true:则执行 patchVnode
  • false:则用 vnode 替换 oldVnode

patchVnode 函数做的工作

  1. 找到对应的真实 dom,称为 el
  2. 判断 vnode 和 oldVnode 是否指向同一个对象。
  • 如果是,那么直接 return。
  • 如果他们都有文本节点并且不相等,那么将 el 的文本节点设置为 vnode 的文本节点。
  • 如果 oldVnode 有子节点而 vnode 没有,则删除 el 的子节点。
  • 如果 oldVnode 没有子节点而 vnode 有,则将 vnode 的子节点真实化之后添加到 el
  • 如果两者都有子节点,则执行 updateChildren 函数比较子节点。

这一步很重要其他几个点都很好理解,所以这里我们来详细来讲一下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)
    }
}

首先介绍下这个函数中的变量定义
(oldStartIdx = 0):oldVnode 的 startIdx, 初始值为 0
(newStartIdx = 0):vnode 的 startIdx, 初始值为 0
(oldEndIdx = oldCh.length - 1):oldVnode 的 endIdx, 初始值为 oldCh.length - 1
(oldStartVnode = oldCh[0]):oldVnode 的初始开始节点
(oldEndVnode = oldCh[oldEndIdx]):oldVnode 的初始结束节点
(newEndIdx = newCh.length - 1):vnode 的 endIdx, 初始值为 newCh.length - 1
(newStartVnode = newCh[0]):vnode 的初始开始节点
(newEndVnode = newCh[newEndIdx]):vnode 的初始结束节点
当 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 时,执行如下循环判断:

  1. oldStartVnode 为 null,则 oldStartVnode 等于 oldCh 的下一个子节点,即 oldStartVnode 的下一个兄弟节点
  2. oldEndVnode 为 null, 则 oldEndVnode 等于 oldCh 的相对于 oldEndVnode 上一个子节点,即 oldEndVnode 的上一个兄弟节点
  3. newStartVnode 为 null,则 newStartVnode 等于 newCh 的下一个子节点,即 newStartVnode 的下一个兄弟节点
  4. newEndVnode 为 null, 则 newEndVnode 等于 newCh 的相对于 newEndVnode 上一个子节点,即 newEndVnode 的上一个兄弟节点
  5. oldEndVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldStartVnode, newStartVnode),执行完后 oldStartVnode 为此节点的下一个兄弟节点,newStartVnode 为此节点的下一个兄弟节点
  6. oldEndVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldEndVnode, newEndVnode),执行完后 oldEndVnode 为此节点的上一个兄弟节点,newEndVnode 为此节点的上一个兄弟节点
  7. oldStartVnode 和 newEndVnode 为相同节点则执行 patchVnode(oldStartVnode, newEndVnode),执行完后 oldStartVnode 为此节点的下一个兄弟节点,newEndVnode 为此节点的上一个兄弟节点
  8. oldEndVnode 和 newStartVnode 为相同节点则执行 patchVnode(oldEndVnode, newStartVnode),执行完后 oldEndVnode 为此节点的上一个兄弟节点,newStartVnode 为此节点的下一个兄弟节点
  9. 使用 key 时的比较:
           oldKeyToIdx为未定义时,由 key 生成 index 表,具体实现为  createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx),

createKeyToOldIdx 的代码如下:

function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap {
  let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch;
  for (i = beginIdx; i <= endIdx; ++i) {
    ch = children[i];
    if (ch != null) {
      key = ch.key;
      if (key !== undefined) map[key] = i;
    }
  }
  return map;
}

在createKeyToOldIdx 方法中,用 oldCh 中的 key 属性作为键,而对应的节点的索引作为值。然后再判断在 newStartVnode 的属性中是否有 key,且是否在 oldKeyToIndx 中找到对应的节点。

  • 如果不存在这个 key,那么就将这个 newStartVnode 作为新的节点创建且插入到原有的 root 的子节点中,然后将 newStartVnode 替换为此节点的下一个兄弟节点。
  • 如果存在这个key,那么就取出 oldCh 中的存在这个 key 的 vnode,然后再进行 diff 的过程,并将 newStartVnode 替换为此节点的下一个兄弟节点。

当上述 9 个判断执行完后,oldStartIdx 大于 oldEndIdx,则将 vnode 中多余的节点根据 newStartIdx 插入到 dom 中去;newStartIdx 大于 newEndIdx,则将 dom 中在区间 [oldStartIdx, oldEndIdx]的元素节点删除

到此 Diff 算法的执行过程结束!

你可能感兴趣的:(大前端,vue,前端)