虚拟 DOM

文章目录

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

javascript 复制代码
// 真实 DOM
<div class="box">
  <h1>Hello</h1>
  <p>World</p>
</div>

// 虚拟 DOM(VNode)
const vnode = {
  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 基本结构

javascript 复制代码
// 简化的 VNode 结构
const vnode = {
  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 的类型

javascript 复制代码
// 文本节点
{ 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 简化的实现

javascript 复制代码
// 创建 VNode
const h = (type, props, ...children) => ({
  type,
  props: props ?? {},
  children: children.flat()
})

// 渲染 VNode 到真实 DOM
const render = (vnode, container) => {
  const el = 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 (typeof child === 'string') {
      el.appendChild(document.createTextNode(child))
    } else {
      render(child, el)
    }
  })
  
  container.appendChild(el)
}

// 使用
const app = h('div', { class: 'app' },
  h('h1', null, 'Hello'),
  h('p', null, 'World')
)

render(app, document.getElementById('root'))

3.3 简化的 Diff 算法

javascript 复制代码
// 比较新旧 VNode
const diff = (oldVNode, newVNode) => {
  // 节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newVNode }
  }
  
  // 文本节点内容变化
  if (typeof newVNode.children === 'string' && 
      oldVNode.children !== newVNode.children) {
    return { type: 'TEXT', text: newVNode.children }
  }
  
  // 属性变化
  const propPatches = diffProps(oldVNode.props, newVNode.props)
  
  // 子节点变化
  const childPatches = 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 的问题

javascript 复制代码
// ❌ 错误:使用 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

// 结果:所有输入框的内容错位!
javascript 复制代码
// ✅ 正确:使用唯一 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 DOM Virtual DOM
定义 浏览器原生组件封装技术(Web Components) 框架层的抽象
目的 封装组件内部结构和样式 高效更新 DOM
实现 浏览器原生支持 JavaScript 对象树
使用 Web Components 标准 React / Vue 等框架

5.2 Shadow DOM 示例

javascript 复制代码
// Web Components 使用 Shadow DOM
class MyCard extends HTMLElement {
  constructor() {
    super()
    const shadow = 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)优化移动操作:

javascript 复制代码
// 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 会销毁旧实例并创建新实例:

vue 复制代码
<Component :key="uniqueId" />

总结

  • 虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级抽象
  • VNode 是虚拟 DOM 的基本单位,包含类型、属性、子节点等信息
  • 更新流程:状态变化 → 生成新 VNode → Diff 比较 → 最小变更 → 更新 DOM
  • Key 的作用:帮助 Diff 算法高效识别节点,保证状态正确性
  • 虚拟 DOM 的价值:声明式开发、批量更新、跨平台,而非"比直接操作 DOM 快"
相关推荐
2401_878454531 小时前
前端高频得手写题
前端
初一初十2 小时前
vue3实现的纯前端护肤品商城网站
前端·javascript·vue.js·前端框架
卷帘依旧2 小时前
React状态管理方案怎么选
前端
zeqinjie2 小时前
Flutter 折叠屏 iPad / 宽屏适配实践
android·前端·flutter
小村儿2 小时前
连载13- 内部Tools,Claude Code 怎么真正"动"你的代码
前端·后端·ai编程
IT_陈寒2 小时前
Python的线程池把我坑惨了,原来异步不是万能的
前端·人工智能·后端
ANnianStriver2 小时前
PetLumina 07 — 宠物管理升级与 JavaScript 大数精度修复
开发语言·javascript·ai编程·宠物
初一初十3 小时前
vue3茶叶商城网站vue网页vuejs前端
前端·javascript·vue.js·vscode·前端框架
kyriewen3 小时前
前端性能优化:LCP 从 4s 到 0.9s 的 5 个核心手段(附配置代码)
前端·javascript·性能优化