虚拟DOM
定义
虚拟DOM是一种用于在前端开发中模拟真实DOM的技术。它是一种抽象的数据结构(简单来说就是一个Javascript对象),用于描述HTML或XML文档的结构和内容。通过将页面的状态和结构保存在内存中,而不是直接操作真实的DOM,虚拟DOM能够减少不必要的DOM操作,从而提高页面性能。
工作原理
虚拟DOM的工作原理主要包括以下几个步骤:
- 状态更新:当页面的状态发生变化时,例如用户输入、数据更新等,虚拟DOM会记录这些变化。
- 差异计算:虚拟DOM会将变化后的虚拟DOM与之前的虚拟DOM进行比较,这个过程通常被称为“diffing”,以计算出最小的差异。
- 更新真实DOM:最后,虚拟DOM会将计算出的差异应用到真实的DOM上,以最小化页面的重新渲染。
优点
虚拟DOM的优点主要体现在以下几个方面:
- 提高性能:通过减少不必要的DOM操作,虚拟DOM能够显著提高页面的渲染效率,从而改善用户体验。
- 简化开发:由于虚拟DOM的存在,前端开发人员可以将更多的精力放在业务逻辑的实现上,而不需要过多地关注DOM的细节。
- 可维护性:虚拟DOM使得前端代码更加模块化和可重用,降低了代码的耦合度,提高了代码的可维护性。
- 跨平台性:虚拟DOM不依赖于浏览器的实现,因此具有跨平台的能力。它可以被应用于不同的前端框架、库以及不同的平台(如Web、移动应用、虚拟现实等)。
Diff算法
vue 虚拟dom和diff算法详解_vue的dom diff算法-CSDN博客
Vue的Diff算法主要用于在数据发生变化时,比较新旧虚拟DOM树(VNode树),找出需要更新的节点,然后仅将这些变化的部分应用到真实的DOM上。这种算法的核心思想是通过比较来避免不必要的DOM操作,从而提高渲染效率。
1. 基本概念
- 最长公共子序列(Longest Common Subsequence, LCS):在两个序列(字符串或文件)中都出现的具有最长长度的子序列。通过找到这些最长公共子序列,可以确定两个文本或文件之间的差异。
- 动态规划(Dynamic Programming):Diff算法的核心原理之一,通过构建一个二维矩阵来记录两个文本或文件之间的对比结果。在矩阵中,每个格子表示当前位置的最长公共子序列的长度。
2. 对比方式
- 行对比(Line-based Diff):将两个文本或文件逐行进行对比,标记出每行中不同的部分。
- 块对比(Hunk-based Diff):将文本或文件按照特定的规则分成块,然后对比这些块之间的差异。这种方式在处理大文件时更为高效。
3. 算法流程
- 初始化:创建一个二维矩阵,用于记录两个文本或文件之间的对比结果。
- 填充矩阵:遍历两个文本或文件的每一行(或块),计算并填充矩阵中的值。每个格子的值表示从文本或文件的开始到当前位置的最长公共子序列的长度。
- 回溯路径:通过矩阵找到最长公共子序列的路径。这个路径揭示了两个文本或文件之间的差异。
- 生成差异文件:根据回溯路径,生成一个差异文件,其中记录了两个文本或文件之间的差异信息。
6分钟彻底掌握vue的diff算法,前端面试不再怕!_哔哩哔哩_bilibili
同级比对
首尾指针法
4. 虚拟DOM中的Diff算法
在前端框架如React和Vue中,Diff算法被用于比较新旧虚拟DOM树之间的差异,并找出需要更新的部分。这些框架中的Diff算法具有以下特点:
- 同层比较:只比较同级元素,不同层级的节点只有创建和删除操作。
- 深度优先:从根节点开始,逐层向下比较。
- 循环从两边向中间比较:在比较过程中,从旧树和新树的末尾开始比较,然后逐渐向中间收拢。
5. 优点
- 性能优化:通过减少不必要的DOM操作,提高渲染效率。
- 灵活性:可以应用于文本比较、文件比较以及虚拟DOM树的比较。
- 可读性:生成的差异文件通常以可读性较高的方式展示差异信息,方便开发者理解和处理。
6. 归纳
Diff算法通过比较两个文本、文件或虚拟DOM树之间的差异,并找出需要更新的部分,从而优化性能并减少不必要的操作。其原理基于最长公共子序列和动态规划的思想,通过构建矩阵和寻找最长公共子序列的路径来确定差异。在前端框架中,Diff算法被广泛应用于虚拟DOM的比较和更新过程中。
snabbdom
render函数
Vue在created–>beforeMount之间的时候会将模板编译成render函数,生成虚拟dom,其参数【vue2中】就是后续的h函数(也可叫createElement函数)
h函数
createElement参数-渲染函数&JSX-CSDNVue进阶技能树
<template> <!-- 注意:这里我们实际上不会使用<template>,因为下面的例子是基于setup函数的渲染 --> </template> <script setup> import { ref, h } from 'vue'; // 假设我们通过props接收todos数组 const props = defineProps({ todos: Array }); // 组件内部数据,用于新待办事项的输入 const newTodo = ref(''); // 添加新待办事项的函数 const addTodo = () => { if (newTodo.value.trim() !== '') { // 假设这里我们通过某种方式(如emit事件)通知父组件更新todos,但为简化示例,我们直接在本地模拟 // 注意:在真实应用中,你应该避免直接修改props,而是通过emit或其他状态管理方案来处理 // 这里仅作为演示如何结合使用props和data props.todos.push({ id: Date.now(), text: newTodo.value }); // 仅为演示,不推荐这样做 newTodo.value = ''; // 清空输入框 } }; // 使用h函数定义渲染逻辑 const render = () => { return h('div', [ h('input', { type: 'text', value: newTodo.value, onInput: (e) => { newTodo.value = e.target.value; }, placeholder: 'Add a new todo...' }), h('button', { onClick: addTodo }, 'Add Todo'), h('ul', props.todos.map(todo => h('li', todo.text))) ]); }; // 导出渲染函数,Vue 3会自动处理这个渲染函数 export default { render }; </script> <style scoped> /* 样式略 */ </style>
patch函数
比较两个虚拟dom根节点是否相同,patch是比较的开始,相当于是diff的入口,diff就是从这一步开始的
patchVnode函数
atchVnode
函数的主要作用是对比两个VNode(新节点和旧节点)之间的差异,并根据这些差异来最小化真实DOM的更新操作。
工作流程
- 判断节点类型:
- 首先,
patchVnode
会检查新旧节点是否是相同类型的节点。这通常包括比较它们的标签名(对于元素节点)或类型(对于组件节点)。- 优化判断:
- 如果新旧节点是相同节点,并且没有发生需要更新DOM的变化(如props、children等),则可能直接跳过更新操作。
- Vue.js还会检查节点是否为静态节点、注释节点、异步组件等,并根据节点类型进行相应的优化处理。
- 属性更新:
- 如果节点的属性发生了变化,
patchVnode
会负责更新这些属性到真实的DOM元素上。这包括事件监听器、样式、类名等的更新。- 子节点更新:
- 如果新旧节点的子节点不同,
patchVnode
会调用updateChildren
函数来更新子节点。updateChildren
函数会执行更复杂的diff算法,以最小化DOM操作并更新子节点。- 文本内容更新:
- 如果节点是文本节点且文本内容发生了变化,
patchVnode
会更新真实DOM元素的文本内容。- 递归处理:
patchVnode
函数会递归地处理新旧节点的所有子节点,确保整个DOM树都得到正确的更新。
updateChildren函数
updateChildren
函数的主要作用是对比新旧子节点列表(通常是VNode数组),找出它们之间的差异,并据此更新真实DOM中的子节点。工作流程
- 初始化指针:
- 将新旧节点列表分别视为两个数组,并设置四个指针(或索引)分别指向它们的头尾。这四个指针分别是新前(newStartIdx)、新后(newEndIdx)、旧前(oldStartIdx)、旧后(oldEndIdx)。
- 循环比较:
- 使用这四个指针进行循环比较,直到新前大于新后或旧前大于旧后为止。在每次循环中,根据当前指针所指向的节点进行比较,并采取相应的操作。
- 节点比较逻辑:
- 相同节点:如果新前节点和旧前节点相同(即它们是同一个VNode),则移动两个指针向后,并可能进行一些必要的DOM更新(如属性更新)。
- 节点移动:如果旧前节点在新节点列表中的位置发生了变化(例如,它现在位于新后节点的位置),则将该节点在DOM中移动到相应的新位置。
- 节点添加/删除:如果在新节点列表中存在而旧节点列表中不存在的节点,则需要在DOM中添加这些新节点;反之,如果旧节点列表中存在而新节点列表中不存在的节点,则需要从DOM中删除这些旧节点。
- 暴力解法:如果上述条件都不满足,且Vue无法直接通过指针移动来确定节点的对应关系,则可能采用暴力解法,即逐个比较新节点与旧节点列表中的节点,并据此更新DOM。
- 优化处理:
- 在比较过程中,Vue会尽量复用旧节点,以减少DOM的创建和销毁操作。这是通过Vue的key机制来实现的,每个VNode都可以有一个唯一的key属性,Vue会基于key来更准确地判断节点的身份和位置。
- 递归处理:
- 如果节点是组件或包含子节点的元素,
updateChildren
函数还会递归地调用自身来处理这些子节点。
VNode(JavaScript对象)
Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode 组合在一起形成一颗树结构,就是虚拟DOM(VDOM)
![]()
VNode是一个类,可以生产不同类型的vnode实例,不同类型的实例表示不同类型的真实DOM。
由于Vue.js对组件采用了虚拟DOM来更新视图,当属性发生变化时,整个组件都要进行重新渲染的操作,但组件内并不是所有的DOM节点都需要更新,所以将vnode缓存并将当前新生成的vnode和缓存的vnode作对比,只对需要更新的部分进行DOM操作可以提升很多的性能。
vnode有很多类型,它们本质上都是Vnode实例化出的对象,其唯一区别是属性不同。
作用:
通过render
将template
模版描述成VNode
,然后进行一系列操作之后形成真实的DOM
进行挂载。VNode存放哪些信息
- data:存储节点的属性,绑定的事件等
- elm:真实DOM 节点
- context:渲染这个模板的上下文对象
- isStatic:是否是静态节点
6. Vnode存放: 在初始化完选项,解析完模板之后,就需要挂载
DOM
了。此时就需要生成VNode
,才能根据VNode
生成DOM
然后挂载。创建出来的VNode
需要被存起来,主要存放在三个位置:parent
,_vnode
,$vnode
。7. VNode类型:VNode有多种类型,以适应不同的DOM节点类型和组件。以下是VNode的一些主要类型:
- 元素VNode:
- 这是最常见的VNode类型,用于表示普通的HTML元素(如
<div>
、<span>
等)。- 它包含元素的标签名、属性、事件监听器、子节点等信息。
- 文本VNode:
- 用于表示纯文本节点。
- 它通常不包含子节点,而是直接在其
text
属性中存储文本内容。- 组件VNode:
- 当Vue组件被渲染时,会生成一个组件VNode。
- 组件VNode的
tag
属性通常是组件的选项对象或构造函数,而不是字符串形式的HTML标签名。- 它还包含传递给组件的props、slots等信息。
- 片段VNode(Fragment VNode):
- 在Vue 2.6+中引入,允许将组件或
render
函数渲染为多个根节点。- 在Vue 3中,组件的默认行为就是作为片段处理的,但在Vue 2中,你需要显式地返回一个包含多个子节点的数组或使用
vue-fragment
插件。- 片段VNode本身不对应真实的DOM元素,而是作为多个子节点的容器。
- 注释VNode:
- 用于表示HTML注释(如
<!-- 这是一个注释 -->
)。- 它不是常见的VNode类型,但在某些情况下(如服务器端渲染或代码生成)可能会用到。
- 克隆VNode:
- 这不是一个独立的VNode类型,但在Vue的某些操作中(如
v-for
的key
处理或插槽内容分发)可能会创建原始VNode的克隆。- 克隆VNode与原始VNode具有相似的结构和属性,但可能在某些属性(如
key
)上有所不同。- 占位符VNode(Portal VNode, Teleport VNode等):
- 在Vue 3中,
Teleport
组件允许你将模板渲染到DOM的另一个位置,而不是在组件的根元素内部。Teleport
组件会生成一个特殊的VNode来表示这种跨DOM树的渲染操作。