一文弄懂Vue中Diff算法

snabbdow Diff算法解读

说明

diff算法算是虚拟DOM的一个核心,也是一个难点.在这块我也是陆陆续续花费了两天才整明白并把这篇博客完成.方法主要是通过看snabbdom的updateChildren函数源码,在本文尾部也贴出来了.

diff 算法的核心

diff 算法的核心是对比新旧节点的 children,更新 DOM

执行过程:

要对比两棵树的差异,可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,然后对比每一次的差异,这样的时间复杂度为 O(n^3),显然这样很低效.然而在DOM 操作时很少会把一个父节点移动/更新到某一个子节点,因此只需要找同级别子节点依次比较,然后依次再找下一级别的节点比较,这样算法的时间复杂度为 O(n) .所以diff算法的核心是对比新旧节点的childern,更新DOM.为了保证真实DOM中的顺序和新节点保持一致可以用while循环首尾节点进行对比,同时对新老节点数组的开始和结尾节点设置标记索引,循环的过程中向中间移动索引,这样既能实现排序也能减小时间复杂度.

在这里插入图片描述

Diff算法过程(对比同级节点)

  • 当新开始节点索引<=新结束索引当旧开始节点索引<=旧结束索引,while循环开始
    • 两两节点比较,有四种比较方式(必须按顺序):
      • 旧开始节点新开始节点比较,如果匹配上了(key 和 sel 相同),那么旧开始节点不动.
        (同时++oldStartIdx,++newStartIdx)开始下一次while循环.
      • 旧结束节点新结束节点比较,如果匹配上了(key 和 sel 相同),那么旧结束节点不动.
        (同时–oldEndIdx,–newEndIdx)开始下一次while循环.
      • 旧开始节点新结束节点比较,如果匹配上了(key 和 sel 相同),那么把旧开始节点移动到旧结束节点的后面.(同时++oldStartIdx,–newEndIdx)开始下一次while循环.
      • 旧结束节点新开始节点比较,如果匹配上了(key 和 sel 相同),那么把旧结束节点移动到旧开始节点的前面.
        (同时–oldEndIdx,++newStartIdx)开始下一次while循环.
    • 如果不是以上四种情况
      • 遍历新节点,使用新节点的 key在老节点数组中找相同节点
        • 如果匹配上了并且是相同节点,把该旧节点(elmToMove.elm)移到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
        • 如果匹配上了但不是相同节点,创建该新节点并插入到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
        • 如果没有匹配上,创建新节点并插入到旧开始节点的前面.
          (同时++newStartIdx)开始下一次while循环.
  • 当新开始节点索引>新结束索引或者当旧开始节点索引>旧结束索引,while循环结束
    • 如果老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx).说明新节点有剩余在初始新节点中,把newStartIdx到newEndIdx中间剩余节点批量插入旧结束节点的后面.循环结束
    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余在初始旧节点中,把oldStartIdx到oldEndIdx之间剩余节点批量删除.循环结束
      在这里插入图片描述
分析如下节点的更新步骤

在这里插入图片描述

注: 这张图是我从网上copy下来的,用作diff算法的分析.如下:

  1. oldStartIdx = 0 ; newStartIdx = 0 ; oldEndIdx = 4, newEndIdx = 2;
  2. oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx条件成立进入while循环
  3. 先进行4种新旧节点的顺序比较:b和a,e和e.e节点匹配成功,e节点位置保持不动,索引向中间移动
    (oldEndIdx = 3, newEndIdx = 1) 该步结束.此时节点顺序为:b a d f e
  4. while循环条件成立,继续判断比较: b和a,f和b,b和b. b节点匹配成功,b移动到旧结束节点f的后面,索引向中间移动
    (oldStartIdx = 1, newEndIdx = 0) 该步结束.此时节点顺序为:a d f b e
  5. while循环条件成立,继续判断比较: a和a. a节点匹配成功,a节点位置保持不动,索引向中间移动
    (oldStartIdx = 2, newStartIdx = 1) 该步结束.此时节点顺序为:a d f b e
  6. newStartIdx>newEndIdx,while循环结束,新节点数组先遍历完成,说明老节点有剩余.
  7. 删除旧节点b a d f e中oldStartIdx = 2 到 oldEndIdx = 3之间对应真实DOM中的节点,也就是d和f.
  8. 节点更新完成.此时节点顺序为:a b e

需要注意的是,图上的标注在移动b的时候位置是错误的应该是在f的后面.

如下是DIff算法核心源码:src/snabbdom.ts
function updateChildren(parentElm: Node,oldCh: VNode[],newCh: VNode[],insertedVnodeQueue: VNodeQueue) {
    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: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 
      // 索引变化后,可能会把节点设置为空
      if (oldStartVnode == null) {
        // 节点为空移动索引
        oldStartVnode = oldCh[++oldStartIdx]; 
        // Vnode might have been moved left
      } 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)) {
        // 1. 比较老开始节点和新的开始节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); 
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 2. 比较老结束节点和新的结束节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); 
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        // Vnode moved right
        // 3. 比较老开始节点和新的结束节点
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); 
        api.insertBefore(parentElm, oldStartVnode.elm!,
        api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) { 
        // Vnode moved left
        // 4. 比较老结束节点和新的开始节点
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); 
        api.insertBefore(parentElm, oldEndVnode.elm!,oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        // 开始节点和结束节点都不相同
        // 使用 newStartNode 的 key 再老节点数组中找相同节点 
        // 先设置记录 key 和 index 的对象
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,oldEndIdx);
        }
        // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引 
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        // 如果是新的vnode
        if (isUndef(idxInOld)) {
          // New element
          // 如果没找到,newStartNode 是新节点
          // 创建元素插入 DOM 树
          api.insertBefore(parentElm, createElm(newStartVnode,
          insertedVnodeQueue), oldStartVnode.elm!);
          // 重新给 newStartVnode 赋值,指向下一个新节点 
          newStartVnode = newCh[++newStartIdx];
        } else {
          // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历 
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) {
            // 如果新旧节点的选择器不同
            // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中 
            api.insertBefore(parentElm, createElm(newStartVnode,
            insertedVnodeQueue), oldStartVnode.elm!);
          } else {
            // 如果相同,patchVnode()
            // 把 elmToMove 对应的 DOM 元素,移动到左边 
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); 
            oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, elmToMove.elm!,
            oldStartVnode.elm!);
          }
          // 重新给 newStartVnode 赋值,指向下一个新节点
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
            // 如果老节点数组先遍历完成,说明有新的节点剩余 
            // 把剩余的新节点都插入到右边
            before = newCh[newEndIdx+1] == null ? null :newCh[newEndIdx+1].elm;
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,insertedVnodeQueue);
      } else {
        // 如果新节点数组先遍历完成,说明老节点有剩余
        // 批量删除老节点
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
}
 
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值