当前位置: 首页 > news >正文

虚拟 DOM

文章目录

  • 前言
  • 一、什么是虚拟 DOM
    • 1.1 定义
    • 1.2 为什么需要虚拟 DOM
    • 1.3 虚拟 DOM 的优势
  • 二、VNode 的结构
    • 2.1 基本结构
    • 2.2 VNode 的类型
  • 三、更新流程
    • 3.1 完整流程
    • 3.2 简化的实现
    • 3.3 简化的 Diff 算法
  • 四、Key 的作用与原理
    • 4.1 定义
    • 4.2 应用场景
    • 4.3 Key 用 index 的问题
    • 4.4 易混淆点
  • 五、Shadow DOM vs Virtual DOM
    • 5.1 区别
    • 5.2 Shadow DOM 示例
  • 六、虚拟 DOM 的性能
    • 6.1 虚拟 DOM 并非总是更快
    • 6.2 Vue 3 的优化
  • 七、易混淆点
  • 八、思考与练习
  • 总结

前言

上一篇讲了 Vue 响应式原理;本篇进入虚拟 DOM——这是 React 和 Vue 等现代框架的核心机制。

虚拟 DOM 解决的核心问题是:如何高效地更新真实 DOM?

直接操作 DOM 很慢,但更慢的是不必要的 DOM 操作。虚拟 DOM 通过在内存中维护一棵 JavaScript 对象树,比较新旧两棵树的差异,然后只更新真正变化的部分,从而实现高效的 DOM 更新。

本篇会讲清楚:

  • 虚拟 DOM 是什么?
  • VNode 的结构是什么样的?
  • 更新流程是怎样的?
  • key属性为什么重要?

一、什么是虚拟 DOM

1.1 定义

虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级表示,每次状态变化生成新的虚拟 DOM 树。

// 真实 DOM<divclass="box"><h1>Hello</h1><p>World</p></div>// 虚拟 DOM(VNode)constvnode={type:'div',props:{class:'box'},children:[{type:'h1',children:'Hello'},{type:'p',children:'World'}]}

1.2 为什么需要虚拟 DOM

问题:直接操作 DOM 很慢,而且很难追踪状态变化。

解决方案

  1. 用 JavaScript 对象描述 DOM 结构(VNode)
  2. 状态变化时生成新的 VNode 树
  3. 比较新旧两棵树的差异(Diff)
  4. 只更新真正变化的部分到真实 DOM

1.3 虚拟 DOM 的优势

  1. 声明式 UI:开发者只需描述目标状态,框架自动处理 DOM 更新
  2. 批量更新:将多次状态变更合并为一次 DOM 更新
  3. 跨平台:同一套 VNode 树可渲染到浏览器 DOM、SSR HTML、Canvas 或移动端原生视图
  4. 可预测性:状态 → 视图的映射是确定性的,便于调试和测试

二、VNode 的结构

2.1 基本结构

// 简化的 VNode 结构constvnode={type:'div',// 标签名或组件props:{// 属性class:'box',onClick:handler},children:[// 子节点{type:'h1',children:'Hello'},{type:'p',children:'World'}],key:'unique-id',// 唯一标识(可选)el:null// 真实 DOM 元素引用(运行时填充)}

2.2 VNode 的类型

// 文本节点{type:null,children:'Hello World'}// 元素节点{type:'div',props:{},children:[]}// 组件节点{type:MyComponent,props:{msg:'hello'}}// Fragment(Vue 3 支持多根节点){type:Fragment,children:[...]}

三、更新流程

3.1 完整流程

1. 状态变化(响应式触发) 2. 生成新的 VNode 树 3. 与旧 VNode 树进行 Diff 比较 4. 计算出最小变更集 5. 批量应用到真实 DOM

3.2 简化的实现

// 创建 VNodeconsth=(type,props,...children)=>({type,props:props??{},children:children.flat()})// 渲染 VNode 到真实 DOMconstrender=(vnode,container)=>{constel=document.createElement(vnode.type)// 设置属性Object.entries(vnode.props).forEach(([key,value])=>{if(key.startsWith('on')){el.addEventListener(key.slice(2).toLowerCase(),value)}else{el.setAttribute(key,value)}})// 递归渲染子节点vnode.children.forEach(child=>{if(typeofchild==='string'){el.appendChild(document.createTextNode(child))}else{render(child,el)}})container.appendChild(el)}// 使用constapp=h('div',{class:'app'},h('h1',null,'Hello'),h('p',null,'World'))render(app,document.getElementById('root'))

3.3 简化的 Diff 算法

// 比较新旧 VNodeconstdiff=(oldVNode,newVNode)=>{// 节点类型不同,直接替换if(oldVNode.type!==newVNode.type){return{type:'REPLACE',newVNode}}// 文本节点内容变化if(typeofnewVNode.children==='string'&&oldVNode.children!==newVNode.children){return{type:'TEXT',text:newVNode.children}}// 属性变化constpropPatches=diffProps(oldVNode.props,newVNode.props)// 子节点变化constchildPatches=diffChildren(oldVNode.children,newVNode.children)return{type:'UPDATE',propPatches,childPatches}}

四、Key 的作用与原理

4.1 定义

  1. key是 Vue 在v-for列表渲染中用于标识每个节点的唯一属性,帮助 Diff 算法高效匹配新旧节点。
  2. Vue 的 Diff 算法通过key判断节点是否可以复用:相同 key 的节点进行 patch 比较,不同 key 则销毁重建。
  3. 不推荐使用index作为 key,因为列表增删会导致索引重排,引起不必要的 DOM 重建和状态丢失。
  4. key不仅用于v-for,在切换动态组件或触发过渡动画时也需要 key 来区分不同元素。

4.2 应用场景

  1. 列表渲染中使用唯一 ID(如item.id)作为 key,保证列表增删时最小化 DOM 操作。
  2. 强制重新渲染组件:给同一组件切换不同的 key 值,Vue 会销毁旧实例并创建新实例。
  3. 表格行数据更新时,使用后端返回的业务 ID 作为 key,避免用户输入框内容错位。

4.3 Key 用 index 的问题

// ❌ 错误:使用 index 作为 key<li v-for="(item, index) in list":key="index"><input v-model="item.name"/></li>// 问题:在列表头部插入新项时// 旧: [A, B, C] → 新: [D, A, B, C]// key=0: A → D (复用 A 的 DOM,但内容变成 D)// key=1: B → A (复用 B 的 DOM,但内容变成 A)// key=2: C → B (复用 C 的 DOM,但内容变成 B)// key=3: 新增 C// 结果:所有输入框的内容错位!
// ✅ 正确:使用唯一 ID 作为 key<li v-for="item in list":key="item.id"><input v-model="item.name"/></li>// 结果:只有新增的 D 需要创建 DOM,其他节点正确复用

4.4 易混淆点

  1. 使用index作 key 在列表只有追加操作时表现正常,但有插入/删除/排序时会出现性能问题和状态错乱。
  2. key 必须是唯一且稳定的值,使用Math.random()作为 key 会导致每次渲染都重建所有节点。
  3. key的作用不仅是优化性能,更重要的是保证组件状态的正确性和 DOM 复用的准确性。
  4. Vue 3 的v-forkey是必须的(会发出警告),Vue 2 中则是可选的。

五、Shadow DOM vs Virtual DOM

5.1 区别

对比项Shadow DOMVirtual DOM
定义浏览器原生组件封装技术(Web Components)框架层的抽象
目的封装组件内部结构和样式高效更新 DOM
实现浏览器原生支持JavaScript 对象树
使用Web Components 标准React / Vue 等框架

5.2 Shadow DOM 示例

// Web Components 使用 Shadow DOMclassMyCardextendsHTMLElement{constructor(){super()constshadow=this.attachShadow({mode:'open'})shadow.innerHTML=`<style> .card { border: 1px solid #ccc; padding: 16px; } </style> <div class="card"> <slot></slot> </div>`}}customElements.define('my-card',MyCard)// 使用// <my-card><p>Hello</p></my-card>

关键区别:Shadow DOM 是浏览器原生的组件封装技术,Virtual DOM 是框架层的抽象,两者完全无关。


六、虚拟 DOM 的性能

6.1 虚拟 DOM 并非总是更快

虚拟 DOM 的核心优势不在于"比直接操作 DOM 快",而在于:

  1. 声明式开发:开发者只需描述目标状态,框架自动计算最优更新
  2. 批量更新:将多次状态变更合并为一次 DOM 更新
  3. 可预测性:状态 → 视图的映射是确定性的

在极少量的 DOM 更新场景下,手动 DOM 操作可能更快。虚拟 DOM 的价值在于复杂场景下的批量更新和跨平台抽象

6.2 Vue 3 的优化

Vue 3 的快速 Diff 算法借鉴了 Inferno,通过预处理最长递增子序列(LIS)优化移动操作:

// Vue 3 的编译时优化// 1. 静态提升:静态节点只创建一次// 2. Patch Flag:标记动态内容类型// 3. Block Tree:只比较动态节点

七、易混淆点

  1. 虚拟 DOM ≠ 更快:对于简单场景(如修改一个文本),直接操作 DOM 可能更快;VNode 的优势在于复杂场景下的批量更新和跨平台抽象。
  2. Shadow DOM ≠ Virtual DOM:Shadow DOM 是浏览器原生组件封装技术(Web Components);Virtual DOM 是框架层的抽象,两者无关。
  3. Key 的作用:Diff 算法通过 key 判断节点是否可复用,key 不稳定(如用 index)会导致错误的节点复用和不必要的 DOM 操作。
  4. Vue 3 的优化:通过编译时优化(静态提升、Patch Flag、Block Tree)减少需要 Diff 的节点数。

八、思考与练习

1.为什么 Vue 3 强制要求v-for必须有key

解析:key帮助 Diff 算法高效识别节点的移动、新增和删除。没有 key 时,Vue 只能按顺序比较,可能导致错误的节点复用和状态丢失。

2.使用index作为 key 在什么场景下会出问题?

解析:列表有插入、删除、排序操作时会出问题。例如在头部插入新项,所有节点的 index 都会变化,导致 DOM 复用错误。

3.虚拟 DOM 的核心价值是什么?

解析:声明式开发 + 批量更新 + 跨平台。开发者只需描述目标状态,框架自动计算最优更新策略。

4.Vue 3 的编译时优化有哪些?

解析:

  • 静态提升:静态节点只创建一次,后续复用
  • Patch Flag:标记动态内容类型(文本、属性等),跳过静态内容
  • Block Tree:只比较动态节点,减少 Diff 范围

5.Shadow DOM 和 Virtual DOM 有什么关系?

解析:没有关系。Shadow DOM 是浏览器原生的组件封装技术(Web Components),Virtual DOM 是框架层的抽象,两者目的和实现完全不同。

6.如何强制重新渲染一个组件?

解析:给组件绑定不同的key值,Vue 会销毁旧实例并创建新实例:

<Component :key="uniqueId" />

总结

  • 虚拟 DOM是用 JavaScript 对象描述真实 DOM 结构的轻量级抽象
  • VNode是虚拟 DOM 的基本单位,包含类型、属性、子节点等信息
  • 更新流程:状态变化 → 生成新 VNode → Diff 比较 → 最小变更 → 更新 DOM
  • Key 的作用:帮助 Diff 算法高效识别节点,保证状态正确性
  • 虚拟 DOM 的价值:声明式开发、批量更新、跨平台,而非"比直接操作 DOM 快"
http://www.gsyq.cn/news/1488137.html

相关文章:

  • 掌握SPT-AKI存档编辑器:从问题诊断到高级定制的完全指南
  • 机器人自主导航终极指南:RTAB-Map环境感知与3D建图实战解密
  • 北京朝阳/东城区黄金回收全攻略:从检测到打款,这三点必须盯紧 - 奢侈品回收测评
  • AI Overviews时代:Google搜索流量的重新分配与应对策略
  • 如何让大语言模型在普通电脑上流畅运行:通义千问Qwen模型优化指南
  • 2026 成都钻石回收科普,详解 4C 评定标准,收的顶教你看懂估价 - 奢侈品回收测评
  • Mermaid Live Editor:5分钟掌握实时图表编辑的终极指南
  • 从DSP56002到DSP56303:嵌入式DSP系统硬件与软件迁移实战指南
  • 一件POLO衫的诞生:全工序解析、工艺难点与自动化设备
  • 跟我一起学“仓颉”编程语言-泛型约束
  • 2026 杭州余杭区翡翠回收五星测评,8 家门店实地走访,教你理性处理闲置首饰 - 奢侈品回收评测
  • 基于EdgeLock安全元件实现充电桩ISO 15118与OCPP 2.0.1安全合规方案
  • 要在 LabVIEW 中灵活地发送和接收 SECS/GEM 消息,避免频繁修改 C# 代码,需要设计一个通用的接口,将消息的构造和解析逻辑从 C# 移到 LabVIEW
  • 惠普游戏本性能控制终极指南:3个简单步骤完全掌控你的设备
  • PyWren完全指南:如何利用云服务实现高效并行计算
  • Vazirmatn字体深度解析:3个关键步骤让波斯语设计更专业
  • 跟我一起学“仓颉”编程语言-泛型练习题
  • 5步实战指南:如何为novel-downloader添加新的小说网站支持
  • Mythos能力门控解析:大模型推理深度与多文档验证的工程化落地
  • 寄快递上门取件,哪个最便宜?2026实测对比 - 快递物流资讯
  • GetQzonehistory:5分钟永久备份QQ空间所有历史记忆的终极方案
  • 厌倦了单调的macOS光标?用Mousecape打造个性化桌面体验的3个实用场景
  • 深入Keil C51:巧用data、xdata和code关键字优化你的51单片机项目内存
  • Rufus:免费USB启动盘制作神器,3分钟搞定Windows 11安装
  • 探索Video2X:AI视频超分辨率与帧插值的深度实践指南
  • 解锁鼠标潜能:Mac Mouse Fix如何让普通鼠标超越苹果触控板
  • WiVRn日志分析:调试与解决流式传输问题的实用技巧
  • 跟我一起学“仓颉”编程语言-Array数组
  • ASP+Access实现的浏览器可用人事管理系统(含论文文档与答辩PPT)
  • 终极鼠标性能解放:Mac Mouse Fix 如何让你的10美元鼠标超越苹果触控板