Vue2 - diff 原理(动图演示)

news2025/5/22 22:56:34

目录

  • 1,diff
    • diff 的时间点
  • 2,_update 函数
  • 3,_patch 函数(进行 diff)
    • 3.1,根节点比较
    • 3.2,子节点比较
  • 4,key的问题
    • 举例1
    • 举例2

1,diff

解释:对比新旧虚拟DOM树,完成对真实DOM的更新,这个对比差异的过程叫做 diff

Vue 会在内部的 patch 函数中完成该过程。

diff 的时间点

当组件创建时,或依赖的数据变化时,会运行一个特定的函数来做2件事:

  • 运行 _render 函数生成新的 VNode tree(虚拟DOM树)
  • 运行 _update 函数,传入新的 VNode tree 的根节点,对比新旧2个树,最终完成对真实DOM的更新。

代码表示大致逻辑:

// vue构造函数
function Vue(){
  // ... 其他代码
  var updateComponent = () => {
    this._update(this._render())
  }
  new Watcher(updateComponent);
  // ... 其他代码
}

diff 就发生在_update函数的运行过程中

Watcher 的作用:简单来说,运行传入的函数(updateComponent),对函数中用到的响应式数据进行依赖收集。

Watcher 的作用具体参考Vue2-数据响应式原理

2,_update 函数

  1. _update 函数接收一个 VNode 参数,也就是this._render() 返回的生成的虚拟 DOM 树。

  2. _update 函数通过当前组件的 this._vnode 属性,拿到的虚拟 DOM 树。

  3. _update 函数首先会给组件的 this._vnode 属性重新赋值,让它指向新树。再判断旧树是否存在:

    • 不存在,说明是第一次加载组件,则通过 patch 函数直接遍历新树,为每个节点生成真实的DOM,并挂载到每个节点的 elm 属性上。(虚拟节点通过 elm 属性指向绑定的真实DOM。)
    • 存在,说明之前已经渲染过组件,则通过 patch 函数对新旧树对比,来实现2个目标:
      • 完成对所有真实 DOM 的最小化处理。
      • 让新树的节点对应合适的真实DOM。

不存在的流程:
不存在的流程
存在时的流程:
不存在时的流程

// 伪代码表示:
function update(vnode) {
  vnode // 新
  this._vnode // 旧
  this._vnode = vnode
}

这样就完成了组件的虚拟DOM树的更新

但还需要解决真实的 DOM 更新(如果不考虑效率,直接用新树生成真实DOM即可)。而为了提升效率,需要对比新旧树,通过实现下面2个目标来提升效率。这个步骤在 _patch 函数中实现。

  • 完成对所有真实 DOM 的最小化处理。
  • 让新树的节点对应合适的真实DOM。

3,_patch 函数(进行 diff)

先来介绍几个术语,方便后续阅读:

  1. 【相同】:指2个虚拟节点的标签(tag)类型、key 值均相同。input 元素还需要考虑 type 属性。

不考虑内容,或后代节点。

<!-- 举例 -->

<!-- 节点相同 -->
<h1>123</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<h1>456</h1> <!-- 对应节点 { tag: h1, key: undefined } -->

<!-- 节点相同 -->
没有标签包裹的文字1  <!-- 对应节点 { tag: undefined , key: undefined } -->
没有标签包裹的文字2  <!-- 对应节点 { tag: undefined , key: undefined } -->

<!-- 节点相同 -->
<h1>123</h1> <!-- 对应节点 { tag: h1, key: undefined } -->
<h1>456</h1> <!-- 对应节点 { tag: h1, key: undefined } -->

<!-- 节点不同 -->
<input type="text" key="_key1"> <!-- 对应节点 { tag: input, key: _key1, data: {attrs: {type: text}} } -->
<input type="radio" key="_key1"> <!-- 对应节点 { tag: input, key: _key1, data: {attrs: {type: radio}} } -->
  1. 【新建元素】:根据一个虚拟节点提供的信息,创建一个真实的 DOM 元素,同时挂载到虚拟节点的 elm 属性上。

  2. 【销毁元素】:运行 vnode.elm.remove()

  3. 【更新】:2个虚拟节点进行对比更新,仅发生在2个虚拟节点【相同】的情况下。

  4. 【对比子节点】:对2个虚拟节点的子节点进行对比。

3.1,根节点比较

在这里插入图片描述
首先会对根节点比较,如果2个虚拟节点

【相同】:进入【更新】流程

  • 将旧节点的真实 DOM 赋值到新节点:newVNode.elm = oldVNode.elm
  • 对比新旧节点的属性,有变化的更新到真实 DOM 中。
  • 当前2个节点处理完毕,开始【对比子节点】

不【相同】:新节点递归的【新建元素】。旧节点直接【销毁元素】。

如果根节点都不相同,则没有对比的必要,直接当做旧树不存在处理。

3.2,子节点比较

diff 的重点

再说明下 diff 的目的:为了修改真实的 DOM,并和新的 VNode tree 对应上

在【对比】子节点时,vue 的实现思路:

  1. 尽量什么也不做。
  2. 不行的话,尽量只改动元素属性。
  3. 还不行,尽量移动元素,而不是删除或创建元素。
  4. 还不行,删除和创建元素。

实现大致逻辑:使用头尾指针+遍历来实现。动图演示(数字代表的是 key,蓝块中的数字代表真实DOM的内容):

在这里插入图片描述

  • 对比新旧指针
    • 一样则进入【更新】流程。
      • 顺序:新旧头指针,新旧尾指针,旧头和新尾,旧尾和新头。
    • 不一样,则以新头指针为基准,看对应的 key 在旧树中是否存在(通过遍历旧树的方式),
      • 存在则进入【更新】流程,并调整真实 DOM 的位置,移动新头指针。
      • 不存在,则创建节点对应的 DOM 元素,
  • 当新头指针超过新尾指针,循环结束。剩下的旧节点如果还是正常的,说明没有处理完,则遍历销毁所有节点对应的真实DOM。旧树不用管,会被垃圾回收。

注意,每个新旧节点【更新】时,都会递归的遍历子节点。

4,key的问题

举例1

for 循环中的 item 如果不使用 key,数据更新(尤其是位置发生了变化)后做 diff 时,会认为原来位置新旧头指针每次指向的虚拟节点都【相同】,则每个节点都会【更新】。如果子节点较多,效率就更低了。

举例:

<template>
  <div>
    <ul>
      <li v-for="item in arr" :key="item">{{ item }}</li>
    </ul>
    <button @click="arr.unshift(99 + count++)">头部插入</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      arr: [1, 2, 3, 4, 5],
      count: 0,
    };
  },
};
</script>

因为 key 的存在,

  • 翻转数组时,也只是位置的移动,不会对比内容更新。
  • 头部插入时,只创建一个DOM,其他的DOM不做变动。否则逐一对比更新。

key 的效果:

在这里插入图片描述

不加 key 的效果:

在这里插入图片描述

举例2

注意,v-if/v-else 关于 key 的问题,vue3 会自动添加,可以看这篇文章对比 vue2 和 vue3 的变化。

<template>
  <div>
    <div>
      <span @click="isAccoutLogin = true">账号登录</span>
      <span>|</span>
      <span @click="isAccoutLogin = false">手机号登录</span>
    </div>
    <div v-if="isAccoutLogin" key="1">
      <label>账号</label>
      <input type="text" />
    </div>
    <div v-else key="2">
      <label>手机号</label>
      <input type="text" />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isAccoutLogin: true,
    };
  },
};
</script>

不加 key 时,因为节点相同,子节点也相同,所以不做更新。

在这里插入图片描述

key 才会有所区分,而清空输入框。

在这里插入图片描述


以上。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1356064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

杨中科 ASP.NETCore开发效率利器 HOT RELOAD

HOT RELOAD 1、困惑:修改了服务器端的代码&#xff0c;必须重新运行程序。 2、方法1: [启动 (不调试) ] 3、方法2: .NET 6开始的Hot Reload(热重载) 正常修改代码后 不重启&#xff0c;是无法看到新的数据展示在页面 修改 运行结果&#xff1a; 方式一&#xff1a;设置开始…

excel 插件:Rainbow Analyst Crack

一个插件中包含四种 EXCEL 审核工具检测并修复隐藏的电子表格错误 不要满足于更少&#xff0c;四种领先的电子表格审计工具合二为一 Rainbow Analyst&#xff08;因其对颜色编码的独特强大使用而得名&#xff09;结合了世界级电子表格审核功能的多个领域&#xff1a; Excel™ …

ASP.NET Core高级之认证与授权(一)--JWT入门-颁发、验证令牌

阅读本文你的收获 了解认证和授权的作用了解在ASP.NET Core中实现身份认证的技术都有哪些学习基于JWT认证并学会颁发和验证JWT令牌 一、重要的前置概念 在一个系统中&#xff0c;不是所有的功能和资源都能够被自由地访问&#xff0c;比如你存在银行系统里面的资金&#xff0c…

Rust 圣经 阅读 字符、布尔、单元类型

字符类型&#xff08;char&#xff09; Rust 的字符不仅仅是 ASCII &#xff0c;还包含所有的 Unicode 值&#xff0c;包括单个的中文、日文、表情符号等等。 Unicode 值的范围从 U0000 ~ UD7FF 和 UE000 ~ U10FFFF。 因为每个 Unicode 都是 4 个字节编码&#xff0c;所以字符…

Mybatis之多表查询

目录 一、简介 1、使用嵌套查询: 2、使用多个 SQL 语句&#xff1a; 3、使用关联查询&#xff1a; 4、使用自定义映射查询&#xff1a; 二、业务场景 三、示例 1、一对一查询 2、一对多查询 一、简介 MyBatis 是一个优秀的持久层框架&#xff0c;它提供了强大的支持来执…

向日葵远程工具的使用Mysql5.7的安装与配置

目录 一、向日葵远程安装与使用 二、Mysql 5.7 安装与配置 2.1 安装 2.2 Navicat Premium 12 测试连接 本机测试连接 外部访问MySQL测试连接 三、思维导图 一、向日葵远程安装与使用 简介&#xff1a; 向日葵远程控制是一款用于对远程PC进行管理和服务的软件,拥有5秒快速…

二阶偏导数(隐函数)

定义&#xff0c;方法 一般型 复杂型

如何修复 SQL Server 数据库中的恢复挂起状态?

当我们想与关系数据库交互时&#xff0c;SQL 就会出现并帮助用户与数据库进行交互。SQL 从高级语言中获取用户的输入&#xff0c;然后访问将代码转换为机器可理解的形式。SQL 确实会恢复数据库文件&#xff0c;但有时 SQL 服务器恢复暂挂阶段会进入帐户&#xff0c;这会停止恢复…

海外数据中心代理与住宅代理:优缺点全面对比

数据中心代理和住宅代理是为了匿名而开发的&#xff0c;通过替换网站眼中您自己的 IP 地址。然而&#xff0c;它们在价格、功能、性能或最佳用例方面存在一些差异。那么&#xff0c;这些代理类型到底有什么相似点和不同点呢&#xff1f; 一、什么是数据中心代理&#xff1f; 1…

Hive用户自定义函数之UDF开发

在进行大数据分析或者开发的时候&#xff0c;难免用到Hive进行数据查询分析&#xff0c;Hive内置很多函数&#xff0c;但是会有一部分需求需要自己开发&#xff0c;这个时候就需要自定义函数了&#xff0c;Hive的自定义函数开发非常方便&#xff0c;今天首先讲一下UDF的入门开发…

人工智能大模型:定义、发展和应用

⭐简单说两句⭐ ✨ 正在努力的小新~ &#x1f496; 超级爱分享&#xff0c;分享各种有趣干货&#xff01; &#x1f469;‍&#x1f4bb; 提供&#xff1a;模拟面试 | 简历诊断 | 独家简历模板 &#x1f308; 感谢关注&#xff0c;关注了你就是我的超级粉丝啦&#xff01; &…

【基础python】条件语句 | 循环

条件语句 if elif else python中和绝大数语言类似&#xff0c;具有能够判断语句顺序的语法 if elif else 分别对应C的 if else if else if 条件为真进入语句 &#xff0c;反之则不进入 如果if 为假&#xff0c;存在else 则会进入else 如果if 为假 &#xff0c;存在…

数据库分区分表

分区分表 为什么要分库分表 软件时代&#xff0c;传统应用都有这样一个特点&#xff1a;访问量、数据量都比较小&#xff0c;单库单表都完全可以支撑整个业务。随着互联网的发展和用户规模的迅速扩大&#xff0c;对系统的要求也越来越高。因此传统的MySQL单库单表架构的性能问…

探索 Vue 实例方法的魅力:提升 Vue 开发技能(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Spring的bean的生命周期!!!

一.单例模式 单例&#xff1a;[启动容器]--->通过构造方法&#xff08;创建对象&#xff09;---->调用set方法&#xff08;注入&#xff09;--->调用init方法&#xff08;初始化&#xff09;----[容器关闭]----->调用destroy方法&#xff08;销毁&#xff09; app…

教你用python画图—Turtle详细教程

Turtle模块绝对是吸引非专业代码开发者人员学习python入门的好工具 通过turtle几行代码的执行软件就会画出漂亮的图形&#xff0c;美观而且有成就感&#xff0c;这样一下子对python编程就产生了兴趣。 这些漂亮的图形如三角形、五角星、机器猫等。在写代码的时候改变几个参数…

反距离加权水平内插,附matlab代码(ERA5和GNSS站点不并址的处理方法之水平补偿)

1.内插方法 我在学习过程&#xff0c;内插方法为反距离加权水平内插&#xff0c;分享我的方法和公式&#xff0c;以及matlab代码。 2.使用该内插法的原因 GNSS与ERA5格网位置不并址&#xff0c;需要进行水平方向和垂直方向的补偿的补偿获得。水平方向不并址如第3节图所示&am…

数字人私人定制

数字人是什么&#xff1f; 在回答这个问题之前&#xff0c;我们先回答另一个问题&#xff0c;人如何与人工智能交流&#xff1f;目前可以通过文字、语音、电脑屏幕、手机屏幕、平板、虚拟现实设备等和人工智能交流&#xff0c;为了得到更好的交流体验&#xff0c;人工智能必然…

css sourcemap 源代码映射

vue.config.js css: {// Enable CSS source maps.sourceMap: process.env.NODE_ENV ! production, }重新运行&#xff1a;yarn serve 效果&#xff1a;