面试官:说一说 vue3 的快速 diff 算法(一)

预处理

文本预处理

在讨论 vue3 的快速 diff 算法前,我们要先了解一下纯文本 diff 算法的预处理

现在有如下两段文本:

js 复制代码
const text1 = 'hello small world'
const text1 = 'hello big world '

经过预处理后,剩下的文字部分就是我们需要进行 diff 操作的部分;针对 text1 和 text2 来说,就是 smallbig

而 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 时,说明在预处理过程中,所有旧子节点都处理完毕了。
但在新子节点中,从 inewEnd 这个区间内的节点都没有被处理,这些节点实际上都是需要被挂载的新节点。

我们可以通过下面代码来实现这部分逻辑:

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 时,说明在预处理过程中,所有新子节点都处理完毕了
但在旧子节点中,从 ioldEnd 这个区间内的节点都没有被处理,这些节点实际上都是需要被卸载的多余节点。

基于上述逻辑,我们可以同样使用 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 算法针对新旧子节点的预处理过程。

  1. 用一个指针 i 指向新旧子节点的头结点 ;开启 while 循环遍历新旧子节点的前置节点,针对相同前置节点使用 patch 打补丁,当遇到不同的节点时停止循环;
  2. 两个指针 newEnd、oldEnd 分别指向新旧子节点的尾结点 ;开启 while 循环遍历新旧子节点的后置节点,针对相同后置节点使用 patch 打补丁,当遇到不同的节点时停止循环;
  3. 当步骤 1、步骤 2 完成以后可能存在几种情况:
    • oldEnd <= i && newEnd <= i:说明新旧子节点全部都处理完毕了;
    • oldEnd < i && newEnd >= i:说明 inewEnd 区间内的节点需要被挂载;
    • newEnd < i && oldEnd >= i:说明 ioldEnd 这个区间内的节点都需要被卸载;
    • 如果以上几种情况都不满足:那么说明新旧子节点在经过预处理后都有剩余节点没被处理到;那么接下来就要考虑是否存在需要移动节点的情况,这也是快速 diff 的核心逻辑。

埋个坑,下一篇开启 vue3 diff 算法核心部分~

相关推荐
你挚爱的强哥3 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
天天进步20156 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
想自律的露西西★7 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳7 小时前
vue3:瀑布流
前端·javascript·vue.js
uzong8 小时前
7 年 Java 后端,面试过程踩过的坑,我就不藏着了
java·后端·面试
程序媛-徐师姐9 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
余道各努力,千里自同风10 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js