Vue2 - diff 原理(动图演示)

目录

  • 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.mzph.cn/news/596483.shtml

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

相关文章

软件测试基础理论学习-软件测试方法论

软件测试方法论 软件测试的方法应该建立在不同的软件测试类型上&#xff0c;不同的测试类型会存在不同的方法。本文以软件测试中常见的黑盒测试为例&#xff0c;简述常见软件测试方法。 黑盒测试用例设计方法包括等价类划分法、边界值分析法、因果图法、判定表驱动法、正交试…

神经网络:经典模型热门模型

在这里插入代码片【一】目标检测中IOU的相关概念与计算 IoU&#xff08;Intersection over Union&#xff09;即交并比&#xff0c;是目标检测任务中一个重要的模块&#xff0c;其是GT bbox与pred bbox交集的面积 / 二者并集的面积。 下面我们用坐标&#xff08;top&#xff0…

数据结构与算法之美学习笔记:44 | 最短路径:地图软件是如何计算出最优出行路径的?

目录 前言算法解析总结引申 前言 本节课程思维导图&#xff1a; 我们学习了图的两种搜索算法&#xff0c;深度优先搜索和广度优先搜索。这两种算法主要是针对无权图的搜索算法。针对有权图&#xff0c;也就是图中的每条边都有一个权重&#xff0c;我们该如何计算两点之间的最短…

Docker安装Elasticsearch,kibana,ik分词器

安装elasticsearch 下载elasticsearch&#xff0c;查看版本&#xff1a;Elasticsearch Guide [8.11] | Elastic docker pull elasticsearch:7.17.16 查看镜像是否下载成功 docker images 创建网络&#xff0c;因为需要部署kibana容器&#xff0c;要让es和kibana容器互联 …

Spring——Spring基于注解的IOC配置

基于注解的IOC配置 学习基于注解的IOC配置&#xff0c;大家脑海里首先得有一个认知&#xff0c;即注解配置和xml配置要实现的功能都是一样的&#xff0c;都是要降低程序间的耦合。只是配置的形式不一样。 1.创建工程 1.1 pom.xml <?xml version"1.0" encoding…

2024 AIGC 应用层十大趋势;iPhone 遭史上最复杂攻击!丨 RTE 开发者日报 Vol.119

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文…

数据库分区分表

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

【 RF 射频 电缆】 MIL-C-17F 标准 规格

第〇、&#xff1f;&#xff1f; RGXXXXX 第一、应用场景 标准号应用场景–&#xff08;–&#xff09;RG-8 RG-9 RG-11粗缆以太网–RG-58细缆以太网–RG-59 RG-75电视系统–RG-62ARCnet网络和IBM 3270网络–RG142电信设备之间的互连 航空电子机架 雷达 GPS 医疗–RG178通信…

Spring常用注解及模拟用户登录流程示例

注解 Resource注解实现自动注入 (反射)代码块xml配置文件 Autowired注解实现自动化注入代码块xml配置文件 扫描器-四个注解Dao层-RepositoryService层-ServiceController层-Controller测试任意类-Component 常用注解示例-模拟用户登录配置自动扫描的xml文件实体类Userdao层消息…

几个有趣的go服务框架

开篇先吐槽几句&#xff5e; 我个人有一些习惯&#xff0c; 比如在服务设计时会考虑的比较长远&#xff0c;会考虑到到未来的扩展等等…然后程序设计的抽象成度就会比较高&#xff0c;各个模块之间解耦&#xff0c;但这样往往就会带来程序的复杂度提升。 这其实在一些公司里面…

微信小程序 ---- 通过 URLScheme 或 URLLink 从短信、邮件、微信外网页等场景打开小程序

1. 用于短信、邮件、网页、微信内等拉起小程序的方法 《URL Scheme 拉起小程序》《URL Link 拉起小程序》 2. 功能描述 URL Scheme: 该接口用于获取小程序 scheme 码&#xff0c;适用于短信、邮件、外部网页、微信内等拉起小程序的业务场景。目前仅针对国内非个人主体的小程…

奇技淫巧:如何给项目中的RabbitMQ添加总开关

本文主要分享了如何给项目中的RabbitMQ添加总开关&#xff0c;通过简单配置开/关RabbitMQ。 一、需求背景 SpringBoot项目里使用了RabbitMQ&#xff0c;但某些场景下&#xff0c;不希望项目启动时自动检查RabbitMQ连接 例如&#xff1a; 在开发不需要RabbitMQ的功能过程中&…

WEB:探索开源PDF.js技术应用

1、简述 PDF.js 是一个由 Mozilla 开发的开源 JavaScript 库&#xff0c;用于在浏览器中渲染 PDF 文档。它的目标是提供一个纯粹的前端解决方案&#xff0c;摆脱了依赖插件或外部程序的束缚&#xff0c;使得在任何支持 JavaScript 的浏览器中都可以轻松地显示 PDF 文档。 2、…

C语言中指针变量如何使用

一、指针变量的定义与声明 1.1 定义 指针变量是用来存储另一个变量的内存地址的变量。在C语言中&#xff0c;指针变量的类型是指向某个类型的指针。例如&#xff0c;int *p; 表示一个整型指针变量p。 1.2 声明 指针变量的声明分为两种形式&#xff0c;一种是直接声明&#…

linux-磁盘扩容 -- 小黑日常超细教程

hi~ 这次小黑带来的是linux磁盘扩容超细教学&#xff0c;按照步骤来&#xff0c;超容易~ 目录 模拟实验对象&#xff1a; 1、查看磁盘分区和挂载点 2、查看新增磁盘 3、将新磁盘格式化&#xff0c;建立新分区 4、查看vg卷组信息 5、分区添加卷组 6、扩容 问题&…

Android kotlin build.gradle.kts配置

1. 添加 maven 仓库 1. 1. settings配置 1. 1.1. settings.gradle repositories {maven {url https://maven.aliyun.com/repository/public/}mavenCentral() }1. 1.2. settings.gradle.kts repositories {maven {setUrl("https://maven.aliyun.com/repository/public/…

《Redis实战》学习笔记

特点 &#xff1a;1、是一个高性能的key/value内存型数据库 2、支持丰富的数据类型(string,List,Set,ZSet,Hash) 3、支持持久化 内存数据&#xff0c; 可以持久化到硬盘中 4、单进程&#xff0c;单线程 效率高 redis实现分布式锁 一、redis的相关指令 1、flushDB 清空当前…

Flink实时电商数仓之旁路缓存

撤回流的处理 撤回流是指流式处理过程中&#xff0c;两表join过程中的数据是一条一条跑过来的&#xff0c;即原本可以join到一起的数据在刚开始可能并没有join上。 撤回流的格式&#xff1a; 解决方案 定时器&#xff1a;使用定时器定时10s&#xff08;数据最大的时间差值&am…

7.14解数独(LC37-H)

算法&#xff1a; 二维递归&#xff08;递归时需要两层for循环&#xff09; 一个for循环放行 另一个for循环放列 画树&#xff1a; 因为这个树形结构太大了&#xff0c;我抽取一部分&#xff0c;如图所示&#xff1a; 回溯三部曲&#xff1a; 1.确定函数参数和返回值 返…

在Gradle工程中使用checkstyle来规范你的项目

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 系列专栏目录 [Java项…