一文搞懂Vue Diff算法

news2024/11/27 12:55:57

为什么需要diff算法?

对于一个容器(比如我们常用的#app)而言,它的内容一般有三种情况:

1.字符串类型,即是文本。
2.子节点数组,即含有一个或者多个子节点
3.null,即没有子节点

vue中,会将dom元素当作vdom进行处理,我们的HTML Attributes、事件绑定都会现在vdom上进行操作处理,最终渲染成真实dom

Virtual Dom:用于描述真实dom节点的JavaScript对象。

使用vdom的原因在于,如果每次操作都是直接对真实dom进行操作,那么会造成很大的开销。使用vdom时就能将性能消耗从真实dom操作的级别降低至JavaScript层面,相对而言更加优秀。 一个简单的vdom如下:

const vdom = {
    type:"div",
    props:{
        class: "class",
        onClick: () => { console.log("click") }
  },
    children: [] // 简单理解这就是上述三种内容
} 

对于vue节点的更新而言,是采用的vdom进行比较。

diff算法便是用于容器内容的第二种情况。当更新前的容器中的内容是一组子节点时,且更新后的内容仍是一组节点。如果不采用diff算法,那么最简单的操作就是将之前的dom全部卸载,再将当前的新节点全部挂载。

但是直接操作dom对象是非常耗费性能的,所以diff算法的作用就是找出两组vdom节点之间的差异,并尽可能的复用dom节点,使得能用最小的性能消耗完成更新操作。

接下来说三个diff算法,从简单到复杂循序渐进。

简单的Diff算法

为什么需要key?

接下来通过两种情况进行说明为什么需要key

如果存在如下两组新旧节点数组:

const oldChildren = [
  {type: 'p'},
  {type: 'span'},
  {type: 'div'},
]
​
const newChildren = [
  {type: 'div'},
  {type: 'p'},
  {type: 'span'},
  {type: 'footer'},
] 

如果我们是进行正常的比较,步骤应该是这样:

找到相对而言较短的一组进行循环对比

  • 第一个p标签与div标签不符,需要先将p标签卸载,再将div标签挂载。
  • 第一个spam标签与p标签不符,需要先将span标签卸载,再将p标签挂载。
  • 第一个div标签与span标签不符,需要先将div标签卸载,再将span标签挂载。
  • 最后多余一个标签footer存在在新节点数组中,将其挂载即可。

那么我们发现其中进行了7次dom操作,但是命名前三个都是可以复用的,只是位置发生了变化。如果进行复用节点我们需要判断两个节点是相等的,但是现在的已有条件还不能满足。

所以我们需要引入key,它相当于是虚拟节点的身份证号,只要两个虚拟节点的type和key都相同,我们便认为他们是相等的,可以进行dom的复用。

这时我们便可以找到复用的元素进行dom的移动,相对而言会比不断的执行节点的挂载卸载要好。

但是,dom的复用不意味不需要更新:

const oldVNode = {type: 'p', children: 'old', key: 1}
const newVNode = {type: 'p', children: 'new', key: 2} 

上述节点拥有相同的typekey,我们可以复用,此时进行子节点的更新即可。

简单的diff算法步骤

先用一个例子说明整个流程,再叙述其方法

const oldChildren = [
  {type: 'p', children: 'p', key: 1},
  {type: 'span', children: 'span', key: 2},
  {type: 'div', children: 'div', key: 3},
  {type: 'section', children: 'section', key : 4},
]
​
const newChildren = [
  {type: 'div', children: 'new div', key: 3},
  {type: 'p', children: 'p', key: 1},
  {type: 'span', children: 'span', key: 2},
  {type: 'footer', children: 'footer', key: 5},
] 

为了叙述简单,这里使用不同的标签。整个流程如下:

  • 从新节点数组开始遍历
  • 第一个是div标签,当前的下标是0,之前的下标是2。相对位置并未改变,不需要移动,只需要就行更新节点内容即可。
  • 第二个是p标签,当前的下标是1,之前的下标是0。就相对位置而言,p相对于div标签有变化,需要进行移动。移动的位置就是在div标签之后。
  • 第三个是span标签,当前的下标是2,之前的下标是1。就相对位置而言,p相对于div标签有变化,需要进行移动。移动的位置就是在p标签之后。
  • 第四个标签是footer,遍历旧节点数组发现并无匹配的元素。代表当前的元素是新节点,将其插入,插入的位置是span标签之后。
  • 最后一步,遍历旧节点数组,并去新节点数组中查找是否有对应的节点,没有则卸载当前的元素。

如何找到需要移动的元素?

上述声明了一个lastIdx变量,其初始值为0。作用是保存在新节点数组中,对于已经遍历了的新节点在旧节点数组的最大的下标。那么对于后续的新节点来说,只要它在旧节点数组中的下标的值小于当前的lastIdx,代表当前的节点相对位置发生了改变,则需要移动,

举个例子:div在旧节点数组中的位置为2,大于当前的lastIdx,更新其值为2。对于span标签,它的旧节点数组位置为1,其值更小。又因为当前在新节点数组中处于div标签之后,就是相对位置发生了变化,便需要移动。

当然,lastIdx需要动态维护。

总结

简单diff算法便是拿新节点数组中的节点去旧节点数组中查找,通过key来判断是否可以复用。并记录当前的lastIdx,以此来判断节点间的相对位置是否发生变化,如果变化,需要进行移动。

双端diff算法

简单diff算法并不是最优秀的,它是通过双重循环来遍历找到相同key的节点。举个例子:

const oldChildren = [
  {type: 'p', children: 'p', key: 1},
  {type: 'span', children: 'span', key: 2},
  {type: 'div', children: 'div', key: 3},
]
​
const newChildren = [
  {type: 'div', children: 'new div', key: 3},
  {type: 'p', children: 'p', key: 1},
  {type: 'span', children: 'span', key: 2},
] 

其实不难发现,我们只需要将div标签节点移动即可,即进行一次移动。不需要重复移动前两个标签也就是p、span标签。而简单diff算法的比较策略即是从头至尾的循环比较策略,具有一定的缺陷。

顾名思义,双端diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法

那么双端diff算法开始的步骤如下:

1.比较 oldStartIdx节点 与 newStartIdx 节点,相同则复用并更新,否则
2.比较 oldEndIdx节点 与 newEndIdx 节点,相同则复用并更新,否则
3.比较 oldStartIdx节点 与 newEndIdx 节点,相同则复用并更新,否则
4.比较 oldEndIdx节点 与 newStartIdx 节点,相同则复用并更新,否则

简单概括:

1.旧头 === 新头?复用,不需移动
2.旧尾 === 新尾?复用,不需移动
3.旧头 === 新尾?复用,需要移动
4.旧尾 === 新头?复用,需要移动

对于上述例子而言,比较步骤如下:

上述的情况是一种非常理想的情况,我们可以根据现有的diff算法完全的处理两组节点,因为每一轮的双端比较都会命中其中一种情况使得其可以完成处理。

但往往会有其他的情况,比如下面这个例子:

const oldChildren = [
  {type: 'p', children: 'p', key: 1},
  {type: 'span', children: 'span', key: 2},
  {type: 'div', children: 'div', key: 3},
  {type: 'ul', children: 'ul', key: 4},
]
​
const newChildren = [
  {type: 'div', children: 'new div', key: 3},
  {type: 'p', children: 'p', key: 1},
  {type: 'ul', children: 'ul', key: 4},
  {type: 'span', children: 'span', key: 2},
] 

此时我们会发现,上述的四个步骤都会无法命中任意一步。所以需要额外的步骤进行处理。即是:在四步比较失败后,找到新头节点在旧节点中的位置,并进行移动即可。动图示意如下:

当然还有删除、增加等均不满足上述例子的操作,但操作核心一致,这里便不再赘述。

总结

双端diff算法的优势在于对于一些比较特殊的情况能更快的对节点进行处理,也更贴合实际开发。而双端的含义便在于通过两组子节点的头尾分别进行比较并更新。

快速diff算法

首先,快速diff算法包含了预处理步骤。它借鉴了纯文本diff的思路,这时它为何快的原因之一。

比如:

const text1 = '我是快速diff算法'
const text2 =  '我是双端diff算法' 

那么就会先从头比较并去除可用元素,其次会重后比较相同元素并复用,那么结果就会如下:

const text1 = '快速'
const text2 = '双端' 

此时再进行一些其他的比较和处理便会简单很多。

其次,快速diff算法还使用了一种算法来尽可能的复用dom节点,这个便是最长递增子序列算法。为什么要用呢?先举个例子:

// oldVNodes
const vnodes1 = [
  {type:'p', children: 'p1', key: 1},
  {type:'div', children: 'div', key: 2},
  {type:'span', children: 'span', key: 3},
  {type:'input', children: 'input', key: 4},
  {type:'a', children: 'a', key: 6}
  {type:'p', children: 'p2', key: 5},
]
​
// newVNodes 
const vnodes2 = [
  {type:'p', children: 'p1', key: 1},
  {type:'span', children: 'span', key: 3},
  {type:'div', children: 'div', key: 2},
  {type:'input', children: 'input', key: 4},
  {type:'p', children: 'p2', key: 5},
] 

经过预处理步骤之后得到的节点如下:

// oldVNodes
const vnodes1 = [
  {type:'div', children: 'div', key: 2},
  {type:'span', children: 'span', key: 3},
  {type:'input', children: 'input', key: 4},
  {type:'a', children: 'a', key: 6},
]
​
// newVNodes 
const vnodes2 = [
  {type:'span', children: 'span', key: 3},
  {type:'div', children: 'div', key: 2},
  {type:'input', children: 'input', key: 4},
] 

此时我们需要获得newVNodes节点相对应oldVNodes节点中的下标位置,我们可以采用一个source数组,先循环遍历一次newVNodes,得到他们的key,再循环遍历一次oldVNodes,获取对应的下标关系,如下:

const source = new Array(restArr.length).fill(-1)
// 处理后
source = [1, 2, 0, -1] 

注意!这里的下标并不是完全正确!因为这是预处理后的下标,并不是刚开始的对应的下标值。此处仅是方便讲解。 其次,source数组的长度是剩余的newVNodes的长度,若在处理完之后它的值仍然是-1则说明当前的key对应的节点在旧节点数组中没有,即是新增的节点。

此时我们便可以通过source求得最长的递增子序列的值为 [1, 2]对于index为1,2的两个节点来说,他们的相对位置在原oldVNodes中是没有变化的,那么便不需要移动他们,只需要移动其余的元素。这样便能达到最大复用dom的效果。

步骤

以上述例子来说:

1.首先进行预处理

核心思路便是进行了类似文本预处理的步骤去除头尾重复的节点。其次便是采用了最长递增子序列来复用相对位置没有发生变化的节点,这些节点是不需要移动的,便能最快的复用和更新。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

【软件开发】前后端分离架构下JWT实现用户鉴权

前后端分离架构下JWT实现用户鉴权在【计算机网络】JWT(JSON Web Token)初识 中,我们讲解了 JWT 的基础知识。Token 验证的方式一般是用在前后端分离的软件开发项目中,所以本篇文章将会从前端和后端的角度去考虑 JWT 的实现。前端 …

H5和小程序的区别

近年来,由于社交电商的火爆程度,相较于传统的电商淘宝、京东、天猫这种第三方平台,其余平台的发展也势不可挡。并且第三方平台的竞争过大,成本过高,抢流量越来越难之后,越来越多的商家企业开始转战H5微商城…

基于知识图谱的多模内容创作技术

导读:由于大数据时代的发展,知识呈指数级增长,而知识图谱技术又在近年来逐步火热,因此诞生了利用知识图谱技术进行智能创作的新想法。本文将分享基于知识图谱的多模内容创作技术及应用。主要包括以下四大部分: 百度知识…

Network Configuration Manager固件漏洞管理

固件漏洞可能会使您的企业和客户的敏感数据面临风险,导致黑客容易进入、销售额下降、声誉损失和处罚。为了避免这些事故,识别这些固件漏洞并定期采取纠正措施非常重要。 使用 Network Configuration Manager,你现在可以识别网络设备中的潜在…

【数据结构-JAVA】包装类 泛型

目录 1. 包装类 1.1 基本数据类型和对应的包装类 1.2 装箱和拆箱 1.3 一道面试题 2. 泛型 2.1 什么是泛型 3. 泛型是如何编译的 3.1 擦除机制 4. 泛型的上界 5. 泛型方法 1. 包装类 在 Java 中,由于基本类型不是继承自 Object,为了在泛型代码中可以…

为什么 FIQ 比 IRQ 的响应速度更快?

目录 1、FIQ在异常向量表位于最末 2、FIQ模式有5个私有寄存器 3、FIQ的优先级高于IRQ 1、FIQ在异常向量表位于最末 一般来说,处理器跳转到异常向量表以后,会根据当前的异常源类型,执行下一次的跳转指令,但是FIQ在异常向量表的…

尚医通:项目搭建-提交到Git(六)

(1)前后端概念介绍 (2)搭建项目后端环境 (3)提交到Git仓库 (1)前后端概念介绍 (2)搭建项目后端环境 项目模块构建 hospital-manage:医院接口模拟…

微服务框架 SpringCloud微服务架构 分布式缓存 44 Redis 分片集群 44.5 RedisTemplate访问分片集群

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 分布式缓存 文章目录微服务框架分布式缓存44 Redis 分片集群44.5 RedisTemplate访问分片集群44.5.1 RedisTemplate访问分片集群44 Redis 分片…

手把手教你使用SpringBoot做一个员工管理系统【代码篇·上】

手把手教你使用SpringBoot做一个员工管理系统【代码篇上】1.登录功能2.登录拦截器的实现3.展示员工列表1.登录功能 首先把登录页面的表单提交地址写一个controller <form class"form-signin" th:action"{/user/login}">表单的name属性不可少&#…

13、腾讯云轻量应用服务器挂载文件系统

前言&#xff1a;腾讯云轻量应用服务器腾讯云文件存储&#xff08;Cloud File Storage&#xff0c;CFS&#xff09;系统的使用小记 轻量应用服务器系统版本是windows server 2012r 一、必要概念 1.1 轻量应用服务器 轻量应用服务器&#xff08;TencentCloud Lighthouse&#x…

【MySQL】浅谈事务与隔离级别

文章目录1. 事务概述2. 事务的特性3. 事务的隔离级别1. 事务概述 什么是事务&#xff1f; 在MySQL中的事务&#xff08;Transaction&#xff09;是由存储引擎实现的&#xff0c;在MySQL中&#xff0c;只有InnoDB存储引擎才支持事务。事务处理可以用来维护数据库的完整性&#x…

大数据学习--使用Java API访问HDFS

Java API访问HDFS编写Java程序访问HDFS1、创建Maven项目2、添加相关依赖3、创建日志属性文件4、启动集群HDFS服务5、在HDFS上创建文件编写Java程序访问HDFS 1、创建Maven项目 创建Maven项目 - HDFSDemo 单击【Create】按钮 2、添加相关依赖 在pom.xml文件里添加hadoop和…

react的jsx和React.createElement是什么关系?面试常问

1、JSX 在React17之前&#xff0c;我们写React代码的时候都会去引入React&#xff0c;并且自己的代码中没有用到&#xff0c;这是为什么呢&#xff1f; 这是因为我们的 JSX 代码会被 Babel 编译为 React.createElement&#xff0c;我们来看一下babel的表示形式。 需要注意的是…

Kotlin 原生拓展函数与非拓展函数

先看一下图文 根据定义的性质可分为两类 非拓展函数 repeat 循环函数,可使用该函数执行一些有限循环任务,务必在构造函数传入循环次数 repeat(repeatNumber:Int 1) with 条件补充区域,可在某些需要两个或者多个函数对象直接的属性进行依赖操作时使用 …

Python 读取图像方式总结

读取并显示图像 opencv3库scikit-image库PIL库读取图像结果分析 打印图像信息 skimage获取图像信息PIL获取图像信息 读取并显示图像方法总结 PIL库读取图像Opencv3读取图像scikit-image库读取图像参考资料 学习数字图像处理&#xff0c;第一步就是读取图像。这里我总结下如何…

深度学习——CPU,GPU,TPU等硬件说明(笔记)

目录 深度学习硬件&#xff1a;CPU和GPU 深度学习硬件&#xff1a;TPU 深度学习硬件&#xff1a;CPU和GPU 1.提升CPU的利用率Ⅰ&#xff1a;提升空间和时间的内存本地性 ①在计算ab之前&#xff0c;需要准备数据 主内存->L3->L2->L1->寄存器 L1&#xff1a;访…

【LeetCode每日一题:1697. 检查边长度限制的路径是否存在~~~并查集+数组排序+排序记录下标位置】

题目描述 给你一个 n 个点组成的无向图边集 edgeList &#xff0c;其中 edgeList[i] [ui, vi, disi] 表示点 ui 和点 vi 之间有一条长度为 disi 的边。请注意&#xff0c;两个点之间可能有 超过一条边 。 给你一个查询数组queries &#xff0c;其中 queries[j] [pj, qj, li…

抖音商家引流的正确方法,抖音商家引流脚本实操教程。

大家好我是你们的小编一辞脚本&#xff0c;今天给大家分享新的知识&#xff0c;很开心可以在CSDN平台分享知识给大家,很多伙伴看不到代码我先录制一下视频 在给大家做代码&#xff0c;给大家分享一下抖音商家引流脚本的知识和视频演示 不懂的小伙伴可以认真看一下&#xff0c…

【lssvm回归预测】基于遗传算法优化最小二乘支持向量机GA-lssvm实现数据回归预测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

图书商城小程序开发,实现图书便捷式选购

1995年联合国教文组织将4月23日规定为世界读书日&#xff0c;由此可见对全世界人民来说读书都是一件很重要的事。并且据调查数据显示&#xff0c;去年我国成年国民图书阅读量达到了59.7%&#xff0c;同比增长了0.2个百分点&#xff1b;人均纸质图书阅读量为4.76&#xff0c;较上…