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

相关推荐
清灵xmf15 分钟前
揭开 Vue 3 中大量使用 ref 的隐藏危机
前端·javascript·vue.js·ref
测试界柠檬22 分钟前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
学习路上的小刘29 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&29 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
Redstone Monstrosity1 小时前
字节二面
前端·面试
冯宝宝^2 小时前
基于mongodb+flask(Python)+vue的实验室器材管理系统
vue.js·python·flask
UestcXiye2 小时前
面试算法题精讲:求数组两组数差值和的最大值
面试·数据结构与算法·前后缀分解
严格格2 小时前
三范式,面试重点
数据库·面试·职场和发展
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel
森叶2 小时前
Electron-vue asar 局部打包优化处理方案——绕开每次npm run build 超级慢的打包问题
vue.js·electron·npm