Vue2 vs Vue3的diff算法
发布于 2 年前 作者 jqiao 2312 次浏览 来自 分享

Vue3发布已经很长时间了,而Vue2将于今年年底停止维护,Vue3Vue2的基础上进行了全面升级和改进,使得Vue3更加强大、高效和易用。其中在diff算法这块做了很大的突破。那我们就来看看吧。

简单diff算法

我们首先忽略掉vue2vue3diff算法。 看下面的这块代码

const oldVNode = {
  type: "div",
  children: [{ type: "p", children: " 1" }],
  children: [{ type: "p", children: " 2" }],
  children: [{ type: "p", children: " 3" }],
};

const newVNode = {
  type: "div",
  children: [{ type: "p", children: " 4" }],
  children: [{ type: "p", children: " 5" }],
  children: [{ type: "p", children: " 6" }],
};

oldVNode是老的节点,newVNode是新的节点。如果我们想用newVNode替换oldVNode该如何操作呢?可能很多想到的必将方便的操作“删除所有的旧的子节点,然后挂载所有新的子节点”。可能我们仔细的想一想,这种操作真的方便吗?可能对于我们的大脑是真的方便,但是对于浏览器可不是哦,这种方式会频繁的操作DOM。这样对于性能是非常的不友好的。
我们根据上面的代码可以发现,如果说节点都是p,只是内容发生了变化,那是不是可以直接修改内容就可以了?这样就只需要把children在页面上渲染出来的内容修改掉即可。但是这种情况必须要将旧子节点的标签和新子节点的标签一一对比。看下下面的代码:

const oldVNode = [
  { type: "p", children: " 1" },
  { type: "p", children: " 2" },
  { type: "p", children: " 3" },
];

const newVNode = [
  { type: "p", children: " 3" },
  { type: "p", children: " 1" },
  { type: "p", children: " 2" },
];

我们发现旧子节点的标签在新子节点中的只是位置改变了,那我们就不需要去修改内容了,只需要通过移动的方式就可以更新了。那如何移动呢?要移动前提是要保证两个节点是同一个节点,那如何判断两个节点是同一个节点呢?这时候就需要__key__了。

const oldVNode = [
  { type: "p", children: " 1", key: " 1" },
  { type: "p", children: " 2", key: " 2" },
  { type: "p", children: " 3", key: " 3" },
];

const newVNode = [
  { type: "p", children: " 3", key: " 3" },
  { type: "p", children: " 1", key: " 1" },
  { type: "p", children: " 2", key: " 2" },
];

通过标签相同和key相同可以判定这两个节点是同一节点。那现在就可以知道旧子节点的第一个节点在新子节点是第二个,旧子节点的第二个节点在新子节点中是第是三个,旧子节点的第三个节点在新子节点中是第一个。
所以我们需要这样移动

需要移动两步才能得到想要的结果。我们在仔细观察,是不是只需要将key:3的节点移动到最前面就可以了。

这样一步操作就可以了。那就得使用vue2的__双端diff算法__

双端diff算法

所谓“双端diff算法”就是从新旧节点的两端开始比较,遇到不同的节点就停止比较。看下源码中是怎么操作的

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) { 
        // 老的开始节点 和 新的开始节点一样
       ···
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老的结束节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老的开始节点 和 新的结束节点一样
       ···
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老的结束节点 和 新的开始节点一样
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 将老的结束节点 塞到 老的新节点之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {  // 非理想状态下的处理方式
        ···
      }
    } 
  }

我们拿一个例子来说:

用上面的案例结合上面的源码来说明一下:
首先判断新旧头部节点是否相同,如果相同则将头部指针向后移动一位,继续比较下一个节点。如果不相同则进行下一步
判断新旧尾部节点是否相同,如果相同则将尾部指针向前移动一位,继续比较前一个节点。如果不相同则进行下一步
判断旧节点的头部节点和新节点的尾部节点是否相同,如果相同则说明旧节点的头部节点需要移动到新节点的尾部(注意这里的尾部是指此时尾指针所指向的位置)。如果不相同则进行下一步
判断旧节点的尾部节点和新节点的头部节点是否相同,如果相同则说明旧节点的尾部节点需要移动到新节点的头部(注意这里的头部是指此时头指针所指向的位置)。如果不相同则进行下一步。

上面那张图就是新旧头部节点不相同,新旧尾部节点也相同,旧节点的头部节点和新节点的尾部节点也不相同,但是旧节点的尾部节点和新节点的头部节点是相同的。所以旧节点的尾部节点需要移动到新节点的头部。

既然Vue2的双端diff算法那么优秀了,Vue3又做了什么改进使之更加优秀?

Vue3的diff算法

Vue2diff算法具有简单、高效、适用于大部分情况等优点。但是该算法也存在一些缺陷,例如在处理列表中节点的移动操作时,需要进行大量的操作,因此在节点数量较多时会导致性能问题。

Vue3中的diff算法对于双端diff做了优化。但是只是对新旧节点的头部和尾部进行对比。从而__确定新旧节点中无序的部分__。比如

对于无序的部分Vue3使用了最长递增子序列。来确定不需要移动的节点。下面我们来根据上图具体分析一下。在无序节点的部分,存储新子序列中的节点在旧子序列中的索引。比如上图所存的索引为[3,4,2,0]。由于0代表是新增的索引,索引初始的索引不能从0开始,则从1开始,索引已经存在的索引在旧子节点中对应的索引要+1。通过算法算出上面所存储索引的最长递增子序列为[0,1]。表示第一个节点和第二个节点是递增的,不需要移动,所以只需要移动两次即可。

这样的操作相比于Vue2的双指针算法,它可以更快地执行,减少了不必要的比较操作。

补充

Vue3对比Vue2除了双端比较算法的优化,还有一些其他地方的优化

  • 节点处理策略的优化:Vue3的diff算法采用了静态标记,避免了对不需要比较的节点进行比较,减少了比较的次数,提高了效率。而且,Vue3的diff算法引入了vnodes的类型化,使得对于不同类型的节点采用不同的比较策略,比如对于纯文本节点,可以直接进行字符串比较,而不需要遍历子节点。
  • 动态节点处理的优化:Vue3的diff算法支持了动态插槽,可以更好地处理动态节点,使得在处理包含动态节点的组件时,性能更好。
  • 内存管理的优化:Vue3采用了更高效的虚拟DOM实现,使用WeakMap存储节点信息,减少了对DOM的操作,降低了内存占用。
  • 编译时优化:Vue3的编译器支持了静态提升和静态分析,可以将一些不需要变化的代码提前处理,减少了运行时的计算和比较,提高了性能。
回到顶部