Vue3中的diff算法——diff算法的前4步处理

大家好,我是哈默。今天我们来继续来学习一下 vue3 中的 diff算法。上回,我们说到了diff 算法要处理的几种场景。在明确了 diff 算法要处理的几种场景之后,我们接下来再来看看 vue3 中是如何应用 diff 算法的呢?

diff 算法的 5 大步

首先,我们来看下源码中,diff 算法的步骤分为哪些:

js 复制代码
// vue-next/packages/runtime-core/src/renderer.ts/patchKeyedChildren 中

const patchKeyedChildren = (
    oldChildren, // 旧的一组子节点
    newChildren, // 新的一组子节点
  ) => {
    let i = 0
    // 新的一组子节点的长度
    const newChildrenLength = newChildren.length
    // 旧的一组子节点中最大的 index
    let oldChildrenEnd = oldChildren.length - 1
    // 新的一组子节点中最大的 index
    let newChildrenEnd = newChildrenLength - 1

    // 1. 自前向后比对
    while (i <= e1 && i <= e2) {
      ...
    }

    // 2. 自后向前比对
    while (i <= e1 && i <= e2) {
      ...
    }

    // 3. 新节点多于旧节点,挂载多的新节点
    if (i > e1) {
      if (i <= e2) {
        ...
      }
    }

    // 4. 新节点少于旧节点,卸载多的旧节点
    else if (i > e2) {
      while (i <= e1) {
        ...
      }
    }

    // 5. 乱序
    else {
      ...
    }
  }

可以看到,vue 总共分为 5 步来进行 diff 算法,那么我们依次来看一下每一步都是怎么做的。

第一步:自前向后比对

这里我们简化了一下代码,并将变量名重新命名了一下,下同。

js 复制代码
let i = 0;

while (i <= oldChildrenEnd && i <= newChildrenEnd) {
  const oldVNode = oldChildren[i];
  const newVNode = newChildren[i];

  if (isSameVNodeType(oldVNode, newVNode)) {
    patch(oldVNode, newVNode);
  } else {
    break;
  }
  i++;
}

举例,以下的所有元素都是 li 元素

第一步的话,vue 是先定义了一个指针 i,初始值为 0

然后分别获取第 1 个旧节点,第 1 个新节点,并将它们扔到一个 isSameVNodeType 的函数中。

我们来看下这个 isSameVNode 函数:

js 复制代码
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key;
}

这个函数就是在比较,两个节点的 typekey 是否相同,如果相同,就认为是同一个节点。

那么,对于我们目前的例子而言,第 1 个旧节点,第 1 个新节点,他们的 type 都是 likey 都为 1,所以它们是相同的节点,直接 patch 更新就好。

更新完第一个节点后,i++,依旧满足 while 循环的条件,所以我们继续进入循环。

同理,会更新第二、三个节点,这样子,我们就处理完了所有的子节点了。

最后,我们来观察下,我们这个 while 循环什么时候会提前 break 掉呢?

js 复制代码
if (isSameVNodeType(oldVNode, newVNode)) {
  patch(oldVNode, newVNode);
} else {
  break;
}

可以发现,就是当 isSameVNodeType 返回 false 的时候,所以假如当我们碰到一对新、旧节点,它们不是相同的节点,那么这个循环就会提前 break 掉。

第二步:自后向前比对

在第 1 步自前向后的循环结束之后,就会来到第 2 步 ------ 子后向前比对。

js 复制代码
// 旧的一组子节点中最大的 index
let oldChildrenEnd = oldChildren.length - 1;
// 新的一组子节点中最大的 index
let newChildrenEnd = newChildrenLength - 1;

while (i <= oldChildrenEnd && i <= newChildrenEnd) {
  const oldVNode = oldChildren[oldChildrenEnd];
  const newVNode = newChildren[newChildrenEnd];

  if (isSameVNodeType(oldVNode, newVNode)) {
    patch(oldVNode, newVNode, container, null);
  } else {
    break;
  }
  oldChildrenEnd--;
  newChildrenEnd--;
}

举例,以下的所有元素都是 li 元素 ,注意,new-akeyold-akey 不同:

根据我们上面所说的,首先还是会来到第一步,自前向后比对。

但是当比较第一个新、旧节点的时候,发现:key 不同,所以不是同一个节点,直接会 break 掉,然后就会进入到第二步 ------ 自后向前比对。

自后向前比对 的步骤其实也是和 自前向后比对 的步骤很类似,也是如果是相同的节点,那么就 patch 更新,否则就 break 掉,只不过是倒叙的一个循环而已。

这样一来,所有的子节点也可以更新完毕。

第三步:新节点多于旧节点,挂载多的新节点

上面的例子中,我们的节点数量是相同的。但如果节点数量不同,那么就要涉及到 挂载卸载 的操作了。

我们先来看下 新节点多于旧节点 的情况:

经过第一步、第二步之后,i、oldChildrenEnd、newChildrenEnd 的值如图:

可以看到,我们只需要挂载 inewChildrenEnd 之间的新元素即可。

第四步:新节点少于旧节点,卸载多的旧节点

我们再看下 新节点少于旧节点 的情况:

经过第一步、第二步、第三步之后,i、oldChildrenEnd、newChildrenEnd 的值如图:

可以看到,我们只需要卸载 ioldChildrenEnd 之间的旧元素即可。

第五步:乱序

乱序的代码是比较多的,有 100 多行:

而且,其中还涉及到了 最长递增子序列 这样的新概念,所以我们下次再单独来讲它。

总结

这次,我们一共分析了 vue3diff算法 的前 4 步:

  1. 自前向后比对
  2. 自后向前比对
  3. 新节点多于旧节点
  4. 新节点少于旧节点

通过这几步,我们已经能够处理一些基本的场景了。

相关推荐
IT_陈寒33 分钟前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰1 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
竹林8182 小时前
用 The Graph 查询链上数据实战:从手搓 RPC 到 Subgraph,我的 NFT 项目数据加载快了 10 倍
前端·javascript
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
Awu12273 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
咪库咪库咪3 小时前
Vue3-生命周期
前端
莪_幻尘4 小时前
你的 AI Skill 越多越蠢?Token 上下文爆炸的求生指南
前端·ai编程
lichenyang4534 小时前
从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘
前端
林瞅瞅4 小时前
Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案
前端
kyriewen5 小时前
别再每次都 Google 了:我整理了前端日常最常踩的 10 个 Git 坑,附速查表
前端·javascript·git