面试官:说一说 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 算法核心部分~

相关推荐
天天进步201519 分钟前
Vue项目重构实践:如何构建可维护的企业级应用
前端·vue.js·重构
2402_8575834919 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
小华同学ai22 分钟前
vue-office:Star 4.2k,款支持多种Office文件预览的Vue组件库,一站式Office文件预览方案,真心不错
前端·javascript·vue.js·开源·github·office
k093336 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
醒了就刷牙2 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
黑客老陈5 小时前
面试经验分享 | 北京渗透测试岗位
运维·服务器·经验分享·安全·web安全·面试·职场和发展
@解忧杂货铺6 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
苹果酱05678 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
web1309332039811 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui