【Vue学习】Vue原理—虚拟DOM和diff算法

news2024/9/19 15:50:04

1. 虚拟DOM

1.1 虚拟DOM介绍

主流前端框架(Vue、React)的主要思想是数据驱动视图,以避免不必要DOM操作,从而提高Web应用程序的性能。如何高效的操作DOM,就需要使用虚拟DOM(Virtual DOM, vdom)技术。在Vue的实现中,虚拟DOM是以JavaScript对象的形式存在的,它与实际的DOM具有相同的结构和属性,但它不具有真正的DOM节点,因此可以避免进行昂贵的DOM操作。

Vue在内部使用vnode(Virtual Node)来管理虚拟DOM,vnode是虚拟DOM中的一个节点对象,包含了DOM节点的标签名、属性、子节点等信息。当应用程序状态发生变化时,Vue会创建新的vnode来描述新的虚拟DOM树,然后通过diff算法和渲染优化策略来计算出需要更新的DOM节点,并进行更新操作。

虚拟DOM实现原理如下图所示:

在这里插入图片描述

1.2 使用vnode模拟DOM结构

在Vue中,h函数可以用来创建vnode,它是一个JavaScript对象,包含了描述DOM节点的各种属性和信息。

在这里插入图片描述
下表给出了vnode中常见的属性和它们的含义:

属性名含义
children包含节点的子节点,可以是一个vnode数组或一个字符串
data包含节点的属性、样式等信息的对象,可以使用data来设置节点的各种属性
elm当前节点对应的真实DOM元素,只有在节点被渲染到页面上后才会有值
key节点的唯一标识,用于优化DOM的更新性能
sel 或 tag当前节点的标签名,例如div、p、a
text包含节点的文本内容,只有当节点没有子节点时才会有文本内容
  • 使用vnode模拟DOM结构
<div id="div" class="container">
	<p>abc</p>
	<ul style="font-size: 20px">
		<li>abc</li>
	</ul>
</div>

以上DOM结构片段,可以通过vnode模拟创建,代码如下:

{
	tag: 'div',
	props: {
		id: 'div1',
		className: 'container'
	},
	children: [
		{
			tag: 'p',
			children: 'abc'
		},
		{
			tag: 'ul',
			props: {style: 'font-size: 20px'},
			children: [
				{
					tag: 'li',
					children: 'abc'
				}
			]
		}
	]
}

1.3 通过Snabbdom实现vdom

Snabbdom 是一个轻量级的虚拟DOM库,它提供了一套简单的API,用于创建和操作虚拟DOM,并支持自定义模块,可以灵活地扩展其功能。Snbbdom的设计思路和Vue的虚拟DOM非常相似,都是采用虚拟DOM来提高Web应用程序的性能和可维护性。

以下代码是使用Snabbdom库操作虚拟DOM,通过下面这个案例可以很好的看到虚拟DOM是如何高效操作DOM元素的。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    
    <script>
		const snabbdom = window.snabbdom

		// 定义 patch
		const patch = snabbdom.init([
		    snabbdom_class,
		    snabbdom_props,
		    snabbdom_style,
		    snabbdom_eventlisteners
		])

		// 定义 h
		const h = snabbdom.h

		const container = document.getElementById('container')

		// 生成 vnode
		const vnode = h('ul#list', {}, [
		    h('li.item', {}, 'Item 1'),
		    h('li.item', {}, 'Item 2')
		])
		patch(container, vnode)

		document.getElementById('btn-change').addEventListener('click', () => {
		    // 生成 newVnode
		    const newVnode = h('ul#list', {}, [
		        h('li.item', {}, 'Item 1'),
		        h('li.item', {}, 'Item B'),
		        h('li.item', {}, 'Item 3')
		    ])
		    patch(vnode, newVnode)
		})
   // vnode = newVnode // patch 之后,应该用新的覆盖现有的 vnode ,否则每次 change 都是新旧对比
		
	</script>
</body>
</html>

效果:点击change按钮之后,会更改list中的第二项内容并增加了第三项,从下图可以看到,DOM结构中第一个list项的DOM元素并没有重新渲染,只是渲染了第二个和第三个list项的DOM元素。(新渲染的DOM元素闪红了一下)。
在这里插入图片描述

2. diff 算法

2.1 diff 算法概述

Vue中的diff算法是指在更新虚拟DOM时,对新旧虚拟DOM进行比较,并尽可能地减少DOM操作的算法。使用diff算法比较两棵树的时间复杂度为 O ( n 3 ) O\left(n^3\right) O(n3),如果一棵树有1000个节点,就要执行十亿次计算,那么算法是不可用的。Vue对diff算法进行了优化,使得算法的时间复杂度降到了 O ( n ) O\left(n\right) O(n)

Vue的diff算法的优化策略如下:

  • 只比较同一层级,不跨级比较。

在这里插入图片描述

  • tag和key都相同,则认为是相同节点,递归地进行子节点的比较。
  • tag不相同,则直接删掉重建,不再深度比较。

在这里插入图片描述

2.2 源码—h函数

h()函数通过函数重载的方式定义,当传入函数的参数个数、参数类型不同时,函数会执行不同的内容。h()函数的返回结果是执行 vnode() 函数。
在这里插入图片描述
vnode() 函数返回一个JS对象,也就是虚拟DOM。
在这里插入图片描述

2.3 源码—patch函数

patch() 函数用于比较两个虚拟DOM树的根节点是否相同。patch()函数中,传入两个参数,第一个参数可以是vnode也可以是普通DOM元素,代表旧的vnode;第二个参数是vnode,代表新的vnode。

  • 当第一个参数是DOM元素时,函数会先创建一个空的vnode,然后将这个空的vnode关联到传入的DOM元素上。
  • 如果传入的两个参数都是vnode,且两个vnode相同(它们有相同的 sel 和 key),此时会调用patchVnode()函数,用于比较这两个vnode的子节点。
  • 如果传入的两个vnode不相同,那么会删除旧的vnode,然后根据新的vnode,重建这个删除掉的旧的vnode。
    在这里插入图片描述

2.4 源码—patchVnode函数

patchVnode()函数用于比较相同vnode节点的子集(text、children)。实现函数的流程图如下图所示:
在这里插入图片描述

  • 源码:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
	// 执行 prepatch hook 生命周期
	const hook = vnode.data?.hook;
	hook?.prepatch?.(oldVnode, vnode);
	
	// 设置 vnode.elem,将旧的 vnode 的 elem,赋给新的 vnode
	const elm = vnode.elm = oldVnode.elm!;
	
	// 旧 children
	let oldCh = oldVnode.children as VNode[];
	// 新 children
	let ch = vnode.children as VNode[];
	
	if (oldVnode === vnode) return;
	
	// hook 相关
	if (vnode.data !== undefined) {
	    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
	    vnode.data.hook?.update?.(oldVnode, vnode);
	}
	
	// 如果新的 vnode.text === undefined (意味着新 vnode.children 有值)
	if (isUndef(vnode.text)) {
	    // 新旧都有 children,此时调用 updateChildren 函数
	    if (isDef(oldCh) && isDef(ch)) {
	        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
	        //  新 children 有,旧 children 无 (旧 text 有)
	    } else if (isDef(ch)) {
	        // 清空 text
	        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
	        // 添加 children
	        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
	        // 旧 child 有,新 child 无
	    } else if (isDef(oldCh)) {
	        // 移除 children
	        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
	        // 旧 text 有
	    } else if (isDef(oldVnode.text)) {
	        api.setTextContent(elm, '');
	    }
	
	    // else : vnode.text !== undefined (意味着 vnode.children 无值)
	    // 新的 vnode 的 text 有值,且和旧的 vnode 的 text 值不一样,那么就删除旧 vnode 的 children,
	    // 设置为新 vnode 的 text 值。
	} else if (oldVnode.text !== vnode.text) {
	    // 移除旧 children
	    if (isDef(oldCh)) {
	        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
	    }
	    // 设置新 text 
	    api.setTextContent(elm, vnode.text!);
	}
	hook?.postpatch?.(oldVnode, vnode);
}

2.5 源码—updateChildren函数

updateChildren()函数采用了双端比较策略,即同时从新旧vnode子节点的头部和尾部进行进步,直到两端的指针相遇。
在这里插入图片描述

  • 源码
function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

      // 旧开始 和 新开始 对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      
      // 旧结束 和 新结束 对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 旧开始 和 新结束 对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 旧结束 和 新开始 对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

      // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
  
        // 没对应上,直接插入节点
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];
        
        // 对应上了
        } else {
          // 对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          
          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

3. 总结

Vue框架使用MVVM模式,以动态渲染组件的方式代替了传统的静态渲染,避免了不必要的DOM操作,从而提高了渲染性能。在MMVM模式中,通过Vue的响应式机制来监听状态的变化从而更新视图界面,视图界面的变化通过Vue指令和事件来改变状态。

如何高效的操作DOM,就需要使用虚拟DOM(Virtual DOM, vdom)技术。通过比对两棵新旧虚拟DOM树上的vnode节点,找出最小的更新范围,从而减少DOM操作。diff算法就是用来比较新旧DOM树并完成DOM更新的算法,diff算法的核心包括四个函数,即h函数、patch函数、patchVnode函数、updateChildren函数。

  • h函数在diff算法初始化时,新建vnode节点。
  • patch函数用于比较新旧虚拟DOM树的根节点,当根节点的sel和key不相同时,直接删除旧的vnode,创建新的vnode,不再比较下面的子节点;当根节点的sel和key相同时,此时会调用patchVnode函数,比较该节点的子集(text和children)。
  • patchVnode函数是用来比较两个vnode并更新DOM的核心函数,它会比较新旧vnode节点的text和children。
  • updateChildren函数是当新旧vnode节点都存在children时,对两个vnode的子节点进行比对。使用双端比较策略。如果新旧子节点存在sel和key相等的清空,就会递归调用patchVnode函数,比较其text和children。

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

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

相关文章

信息收集工具使用详解

要求&#xff1a;熟练掌握域名收集、子域名、IP地址收集的各种工具的使用&#xff0c;结果写入实验报告 警示&#xff1a;遵纪守法 工具&#xff1a;kail 信息收集 信息收集又称 网络踩点(footprinting)&#xff0c;攻击者通过各种途径对要攻击的目标进行有计划和有步骤的信息…

除了ChatGPT,这20款AI神器同样值得你使用

2022年是AI技术大发展的一年&#xff0c;特别是ChatGPT的很空出世&#xff0c;让AI工具迎来大爆发&#xff0c;今天就给大家整理出20款免费且实用的AI工具推荐&#xff0c;无论是做设计还是日常学习工作都能用得到。2023年已经开始&#xff0c;让这些AI工具帮你提升生产效率&am…

Docker SYS_ADMIN 权限容器逃逸

1.漏洞原理Docker容器不同于虚拟机&#xff0c;它共享宿主机操作系统内核。宿主机和容器之间通过内核命名空间&#xff08;namespaces&#xff09;、内核Capabilities、CGroups&#xff08;control groups&#xff09;等技术进行隔离。若启动docker容器时给主机一个--cap-addSY…

【el】表格

一、用法1、动态表格后端返回的值&#xff1a;第一个数组是表头&#xff0c;其余是内容<el-tableref"tableHeight":data"tableColumns":height"tableHeight"borderstyle"width: 100%; margin-top: 1%"row-click"rowclick"…

14、字符串处理函数

目录 一、字符串复制 二、字符串连接 三、字符串比较 四、字符串大小写转换 五、获得字符串长度 一、字符串复制 在字符串处理函数中包含strcpy函数&#xff0c;该函数可用于复制特定长度的字符串到另一个字符串中。其语法格式如下&#xff1a; 功能&#xff1a;把源字符…

张驰咨询:用六西格玛方法降低锂电池内部短路缺陷

锂电池作为现代电子设备中最常用的电池类型之一&#xff0c;由于其高能量密度和长寿命等优点&#xff0c;已经广泛应用于手机、笔记本电脑、电动车等领域。然而&#xff0c;在锂电池制造过程中&#xff0c;由于材料、工艺、设备等多种因素的影响&#xff0c;会产生内部短路的问…

基于麻雀算法改进的SVM电器启动识别,基于麻雀算法优化SVM分类预测

目录 摘要 背影 Eggholder测试函数 MATALB编程 测试函数代码 麻雀算法原理 麻雀算法主要参数 麻雀算法流程图 麻雀算法优化测试函数代码 基于麻雀算法改进的SVM电器启动识别 matlab编程实现 效果图 结果分析 展望 摘要 麻雀算法理论&#xff0c;SSA-SVM,电器启动识别&#x…

Android开发之简单控件

文章目录一 文本显示1.1 文本设置的两种方式1.2 常见字号单位类型2.2 设置文本的颜色三 视图基础3.1 设置视图的宽高3.2 设置视图的间距3.3 设置视图的对齐方式四常用布局4.1 线性布局LinearLayout4.2 相对布局RelativeLayout4.3 网格布局GridLayout4.4 滚动视图ScrollView五 按…

【uni-app教程】二、UniAPP 初始化相关配置

二、UniAPP 初始化相关配置 (1)工程目录结构 https://uniapp.dcloud.net.cn/tutorial/project.html#目录结构) 工程简介 一个工程&#xff0c;就是一个 Vue 项目&#xff0c;你可以通过 HBuilderX 或 cli 方式快速创建 uni-app 工程&#xff0c;详见&#xff1a;快速上手。 …

arduino烧录引导程序(BootLoader)方法及出错点

文章目录一、硬件电路准备1、328P的管脚图2、电路连接图&#xff08;1&#xff09;连接图&#xff08;2&#xff09; ISP连接及端口介绍&#xff08;3&#xff09;管脚连接对应表1、准备ArduinoISP2、使用Arduino as ISP烧录设置Arduino板为烧写器3、烧录及错误4、为新烧录328p…

无代码开发浅谈

前 言互联网共享软件工厂KAPT无代码开发平台&#xff0c;提供了可视化编程方法&#xff0c;经过拖拽组件&#xff0c;就像做ppt一样&#xff0c;快速的就能搭建一个软件应用&#xff0c;更高效的构建业务应用程序。以前开发软件大多只能编写代码完成&#xff0c;所以有软件开发…

Mit6.S081-实验1-Xv6 and Unix utilities-pingpong问题

Mit6.S081-实验1-Xv6 and Unix utilities-pingpong问题在进行pingpong实验的时候遇到了许多问题在这里记录一下。 1.输出乱序问题 出现这个问题主要是因为没有弄懂wait(0)的作用&#xff0c; wait(0)暂时停止目前进程的执行&#xff0c;直到信号来到或子进程结束&#xff0c;…

如何让自动化测试框架更自动化?

一、引言 ​对于大厂的同学来说&#xff0c;接口自动化是个老生常谈的话题了&#xff0c;毕竟每年的MTSC大会议题都已经能佐证了&#xff0c;不是大数据测试&#xff0c;就是AI测试等等&#xff08;越来越高大上了&#xff09;。不可否认这些专项的方向是质量智能化发展的方向…

csgo搬砖项目,真的能月入6k?

01 相信很多粉丝都知道steam这个平台&#xff0c;steam是一个游戏平台&#xff0c;这里面的游戏都是要通过去充值购买才能去玩&#xff0c;有的是买游戏的账号&#xff0c;有的是买这个游戏的使用权&#xff0c;买了之后安装到手机上或者电脑上我们才能去畅玩游戏&#xff0c;…

【人工智能 Open AI 】我们程序员真的要下岗了- 全能写Go / C / Java / C++ / Python / JS 人工智能机器人

文章目录[toc]人工智能 AI Code 写代码测试用golang实现冒泡排序用golang实现计算环比函数goroutine and channel用golang实现二叉树遍历代码用golang实现线程安全的HashMap操作代码using C programming language write a tiny Operation Systemuse C language write a tiny co…

【9】SCI易中期刊推荐——工程技术-计算机:软件工程(中科院4区)

🚀🚀🚀NEW!!!SCI易中期刊推荐栏目来啦 ~ 📚🍀 SCI即《科学引文索引》(Science Citation Index, SCI),是1961年由美国科学信息研究所(Institute for Scientific Information, ISI)创办的文献检索工具,创始人是美国著名情报专家尤金加菲尔德(Eugene Garfield…

电脑C盘满了,怎么清理c盘空间?

电脑的C盘是系统盘&#xff0c;存储着操作系统和软件等关键文件&#xff0c;因此当C盘空间不足时&#xff0c;电脑的性能和稳定性都会受到影响。 真实案例&#xff1a;c盘空间莫名其妙变小&#xff1f; “C盘快满了不敢乱删&#xff0c;请问该如何清理&#xff1f;” “求大佬…

八股文(一)

一、箭头函数和普通函数有什么区别 (1)箭头函数比普通函数更加简洁 (2)箭头函数没有自己的this 箭头函数不同于传统JavaScript中的函数&#xff0c;箭头函数并没有属于⾃⼰的this&#xff0c;它所谓的this是捕获其所在上下⽂的 this 值&#xff0c;作为⾃⼰的 this 值&#…

希赛PMP模拟题2022(第9套)

希赛PMP模拟题2022&#xff08;第9套&#xff09; 22 需求和范围 区别 21 什么阶段&#xff1f; 在开发过程中不断衡量 价值 和优先级关系排序20 产品路线图 回顾 团队沟通方式 无法面对面沟通时候 其他人参与 了解项目 需求发布计划 风险 排序发表计划 vs 评审会议 老母鸡 是…

ESP-C3入门14. 实现基本的web server

ESP-C3入门14. 实现基本的web server一、ESP32 IDF创建WEB SERVER的流程1. 配置web服务器2. 注册 URI处理器3. 实现 URI处理器函数4. 处理HTTP请求5. 处理web socket连接6. 注册 URI 处理函数7. 启动HTTP服务器8. 发送响应9. 关闭 http 服务二、本要主要使用API的说明1. httpd_…