预处理
文本预处理
在讨论 vue3 的快速 diff 算法前,我们要先了解一下纯文本 diff 算法的预处理。
现在有如下两段文本:
js
const text1 = 'hello small world'
const text1 = 'hello big world '
经过预处理后,剩下的文字部分就是我们需要进行 diff 操作的部分;针对 text1 和 text2 来说,就是 small
和 big
。
而 vue3 的快速 diff 算法实际上就是借鉴了纯文本 diff 算法的思路,针对新旧节点会先进行预处理。
节点预处理
假设现在有新旧两组子节点:
从图中不难看出,两组子节点具有相同的前置节点 p1,以及相同的后置节点 p3 和 p-4。
我们不需要移动相同的前置节点和后置节点,因为它们在新旧两组子节点中的相对位置不变。但是,我们仍然需要在它们之间打补丁(也就是patch)。
而打补丁的过程,实际上就是遍历对比新旧子节点的过程。
处理前置节点
我们可以使用一个指针i
,指向新旧子节点的头节点:
然后使用一个 while
循环,让指针 i
递增,遇到相同的节点就调用 patch
方法打补丁,直到遇到第一个不相同的节点时停止循环;
具体代码如下:
js
function patchVnode(oldChildren, newChildren) {
// i 指向头结点
let i = 0
// 从新旧子节点的数组中 拿到当前指向的节点
let oldVnode = oldChildren[i]
let newVnode = newChildren[i]
// 这里可以用新旧 vnode 的 key 作比较,来判断是否同一节点
while (oldVnode.key === newVnode.key) {
// 调用 patch,针对新旧 vnode 打补丁
patch(oldVnode, newVnode)
i++
// 更新新旧子节点的值
oldVnode = oldChildren[i]
newVnode = oldChildren[i]
}
}
经过上面这段代码的处理后,我们相同的前置节点就等到了更新:
处理后置节点
接下来,我们需要对新旧节点的后置节点进行一个处理。
我们需要两个索引newEnd和oldEnd,它们分别指向新旧两组子节点中的最后一个节点:
然后,还是用一个 while
循环进行遍历:
js
function patchVnode(oldChildren, newChildren) {
/** 处理前置节点 */
// i 指向头结点
let i = 0
// 从新旧子节点的数组中 拿到当前指向的节点
let oldVnode = oldChildren[i]
let newVnode = newChildren[i]
// 这里可以用新旧 vnode 的 key 作比较 来判断是否同一节点
while (oldVnode.key === newVnode.key) {
// 调用 patch,针对新旧 vnode 打补丁
patch(oldVnode, newVnode)
i++
// 更新新旧子节点的值
oldVnode = oldChildren[i]
newVnode = oldChildren[i]
}
/** 处理后置节点 */
// 让 oldEnd 和 newEnd 分别指向旧、新子节点的最后一个元素
let oldEnd = oldChildren.length - 1
let newEnd = newChildren.length - 1
oldVnode = oldChildren[oldEnd]
newVnode = newChildren[newEnd]
while (oldVnode.key === newVnode.key) {
// 调用 patch,针对新旧 vnode 打补丁
patch(oldVnode, newVnode)
// 这里指针往回走 递减
oldVnode--
newVnode--
// 更新新旧子节点的值
oldVnode = oldChildren[i]
newVnode = oldChildren[i]
}
}
挂载新增节点
经过上一步的处理后,两组子节点的状态如下:
从图中可以看出,剩下未被处理的节点 p2
是一个新增的节点。
通过观察图中索引的位置,我们不难发现:
当满足 oldEnd < i && newEnd >= i
时,说明在预处理过程中,所有旧子节点都处理完毕了。
但在新子节点中,从 i
至 newEnd
这个区间内的节点都没有被处理,这些节点实际上都是需要被挂载的新节点。
我们可以通过下面代码来实现这部分逻辑:
js
function patchVnode(oldChildren, newChildren) {
/** 省略 处理前置节点 */
/** 省略 处理后置节点 */
/** 挂载剩余的新节点 */
if (i > oldEnd && i <= newEnd) {
// 拿到节点挂载的锚点
const anchorIdx = newEnd + 1
// 做一下边界情况处理
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIdx].el : null
// 挂载从 i 到 newEnd 之间的所有节点
while(i <= newEnd) {
let newVnode = newChildren[i]
// 这里的 null 表示没有旧节点,那么 patch 函数会执行挂载逻辑,挂载的锚点就是我们传入的 anchor
patch(null, newVnode, anchor)
i++
}
}
}
卸载多余旧子节点
在上一步中我们考虑了预处理后,存在新增节点的情况;接下来我们来看看另一种情况:
当满足 newEnd < i && oldEnd >= i
时,说明在预处理过程中,所有新子节点都处理完毕了 。
但在旧子节点中,从 i
至 oldEnd
这个区间内的节点都没有被处理,这些节点实际上都是需要被卸载的多余节点。
基于上述逻辑,我们可以同样使用 while
循环来卸载对应节点:
js
function patchVnode(oldChildren, newChildren) {
/** 省略 处理前置节点 */
/** 省略 处理后置节点 */
/** 省略 挂载剩余的新节点 */
if (i > oldEnd && i <= newEnd) {
}
/** 卸载多余旧子节点 */
if (i > newEnd && i <= oldEnd) {
while (i <= oldEnd) {
let oldVnode = oldChildren[i]
// 直接调用 unmount 方法卸载对应节点
unmount(oldVnode)
i++
}
}
}
总结
以上就是 vue3 diff 算法针对新旧子节点的预处理过程。
- 用一个指针 i 指向新旧子节点的头结点 ;开启
while
循环遍历新旧子节点的前置节点,针对相同前置节点使用patch
打补丁,当遇到不同的节点时停止循环; - 用两个指针 newEnd、oldEnd 分别指向新旧子节点的尾结点 ;开启
while
循环遍历新旧子节点的后置节点,针对相同后置节点使用patch
打补丁,当遇到不同的节点时停止循环; - 当步骤 1、步骤 2 完成以后可能存在几种情况:
oldEnd <= i && newEnd <= i
:说明新旧子节点全部都处理完毕了;oldEnd < i && newEnd >= i
:说明i
至newEnd
区间内的节点需要被挂载;newEnd < i && oldEnd >= i
:说明i
至oldEnd
这个区间内的节点都需要被卸载;- 如果以上几种情况都不满足:那么说明新旧子节点在经过预处理后都有剩余节点没被处理到;那么接下来就要考虑是否存在需要移动节点的情况,这也是快速 diff 的核心逻辑。
埋个坑,下一篇开启 vue3 diff 算法核心部分~