Vue3
发布已经很长时间了,而Vue2
将于今年年底停止维护,Vue3
在Vue2
的基础上进行了全面升级和改进,使得Vue3
更加强大、高效和易用。其中在diff
算法这块做了很大的突破。那我们就来看看吧。
简单diff算法
我们首先忽略掉vue2
和vue3
的diff
算法。 看下面的这块代码
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算法
Vue2
的diff
算法具有简单、高效、适用于大部分情况等优点。但是该算法也存在一些缺陷,例如在处理列表中节点的移动操作时,需要进行大量的操作,因此在节点数量较多时会导致性能问题。
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的编译器支持了静态提升和静态分析,可以将一些不需要变化的代码提前处理,减少了运行时的计算和比较,提高了性能。