Vue 系列之:虚拟 DOM
真实 DOM 和虚拟 DOM
真实 DOM
真实 DOM 是浏览器能够直接解析和渲染的节点结构,当我们使用 JavaScript 操作 DOM 时,实际上是在与浏览器提供的 DOM API 进行交互。每次对真实 DOM 的修改(如创建、删除或更新节点)都可能触发浏览器的重排(reflow)和重绘(repaint),这些操作通常比较耗时。
例如,使用原生 JavaScript 创建并添加一个节点:
const div = document.createElement('div');
div.className = 'box';
div.textContent = 'Hello';
document.body.appendChild(div);
虚拟 DOM(VNode)
虚拟 DOM(VNode) 是用 JavaScript 对象来描述真实 DOM 结构的一种方式。它是对真实 DOM 的轻量级抽象,仅包含真实 DOM 的核心信息(如标签名、属性、子节点等)。
例如,一个 <div class="box">Hello</div> 对应的虚拟 DOM 对象可能如下:
const vnode = {
type: 'div', // 节点类型
props: { class: 'box' }, // 节点属性
children: 'Hello' // 子节点
}
Vue 是如何生成虚拟 DOM 的?
注意:Vue 不会“把真实 DOM 转成虚拟 DOM”,而是用虚拟 DOM 来描述和生成真实的 DOM。
Vue 通过 render 函数来创建虚拟 DOM。有以下几种方式:
方式一:手写 render 函数
render(h) {
// h 函数参数:标签名/组件、属性对象、子节点
return h('div', {
class: 'box' // 设置 class 属性
}, 'Hello') // 文本内容作为子节点
}
方式二:使用模板(template)
<template>
<div class="box">
Hello
</div>
</template>
在构建时,Vue 的 模板编译器(template compiler) 会自动把这段 HTML 模板编译成等价的 render 函数。
方式三:使用 JSX(在 Vue 3 中更常见)
render() {
return (
<div class="box">
Hello
</div>
)
}
JSX 最终也会被 Babel 编译成 h() 调用。
渲染函数 render 是生成虚拟 DOM 的核心,其中的 h 函数是创建虚拟节点的关键方法。
知道了虚拟 DOM 是如何创建的,那么虚拟 DOM 如何变成真实 DOM?
Vue 组件在首次挂载时,会:
-
执行 render() 函数,生成 第一个虚拟 DOM(VNode)。
-
通过这个 VNode 创建真实 DOM 并插入页面。
-
此时没有“旧 VNode”用于对比,所以直接全量渲染。
后续更新时(例如响应式数据变化),会:
-
Vue 触发组件重新渲染
-
再次调用 render() 函数,生成新的 VNode。
-
拿这个新的 VNode 和上一次保存的旧 VNode 做 Diff 对比。
-
根据差异更新真实 DOM。
-
更新完成后,新的 VNode 成为下一次的“旧 VNode”,被保存下来。
旧的 VNode 存在哪里?
Vue 会为每个组件实例维护一个内部属性,比如:
vm._vnode = VNode
这个 _vnode 就是上次渲染生成的虚拟 DOM 树。在下一次更新时,它就被当作“旧 VNode”用于 Diff。
旧的 VNode 会一直存在内存中吗?
是的,直到组件被销毁(beforeDestroy 时释放)。
一个简单的流程图:
Vue 使用虚拟 DOM 有哪些优缺点
优点
开发体验优势
Vue 结合虚拟 DOM 将传统的命令式编程变为声明式编程,声明式编程对比命令式编程有着更好的开发体验。开发者只需关注数据状态,无需手动操作 DOM,虚拟 DOM 会根据数据状态自动更新视图。
跨平台能力
虚拟 DOM 作为中间层,使得同一套代码可以渲染到不同平台,例如:
-
Web 平台:渲染为
<div>、<span>等标准标签 -
小程序平台:渲染为
<view>、<text>等小程序组件 -
Native 平台:渲染为原生移动组件
性能优化
-
批量更新:虚拟 DOM 可以将多次零散的 DOM 操作合并为一次,减少重排/重绘次数
例如,向列表中添加 100 个元素:
-
直接操作真实 DOM:需执行 100 次 appendChild,触发 100 次重排。
-
虚拟 DOM:先在 JS 中构建完整的虚拟 DOM 列表,再一次性渲染为真实 DOM,仅触发 1 次重排。
-
-
精准更新:通过 Diff 算法对比新旧虚拟 DOM,只更新变化的部分(而非全量替换)。
例如:列表从 [A, B, C] 变为 [A, D, C]。真实 DOM 会删除 B、插入 D(2 次操作);虚拟 DOM 通过 Diff 发现只有第二个元素变化,直接修改 B 为 D(1 次操作)
缺点
内存占用
需要额外内存来存储虚拟 DOM 树
初始渲染成本
首次渲染需要先创建虚拟 DOM,再转换为真实 DOM
性能缺陷
对于简单的、少量的 DOM 操作,直接操作真实 DOM 可能更高效,因为虚拟 DOM 需要花费时间来计算找不同。
例如一个组件内有 100 个 li,我更新了其中的一个 li,这时候 Vue 框架会进行以下操作:
-
Vue 的响应式系统触发该组件的重新渲染
-
生成新的 VNode 树:组件会执行 render 函数,生成一个由 100 个 li 组成的新的虚拟 DOM 树(VNode) 。
-
执行 Diff 比对:Vue 会将这个新的 VNode 与旧的 VNode 进行比对,最坏的情况要对比 100 次。
-
更新 DOM。
这个过程显然会比直接操作 DOM 更加耗时。
Vue 针对虚拟 DOM 的优化
编译时优化
Vue 模板编译器会分析模板中的节点,将静态节点(内容不会随数据变化的节点)标记为 static,并在虚拟 DOM 生成时进行复用,避免不必要的 Diff 计算。
<template>
<div class="container">
<!-- 静态节点:文本和class不会变化 -->
<h1 class="title">Vue虚拟DOM优化</h1>
<!-- 动态节点:文本随message变化 -->
<p class="content">{{ message }}</p>
</div>
</template>
编译后的虚拟 DOM 生成逻辑:
// 静态节点:仅在初始化时生成一次,后续复用
const staticVnode = {
tag: 'h1',
props: { class: 'title' },
children: ['Vue虚拟DOM优化'],
static: true // 标记为静态节点
};
// 动态节点:数据变化时重新生成
function render() {
return {
tag: 'div',
props: { class: 'container' },
children: [
staticVnode, // 直接复用静态节点
{
tag: 'p',
props: { class: 'content' },
children: [this.message], // 动态文本
static: false
}
]
};
}
组件级更新
Vue 的响应式系统是基于组件级别的依赖追踪。
当一个组件依赖的数据发生变化时,这个组件会重新执行 render 函数(即重新渲染),生成新的虚拟 DOM 树。
当组件重新 render 时,会生成新的虚拟 DOM 树。
然后 Vue 会使用 Diff 算对比这个组件的新旧 VNode 树,逐层比较,找出需要更新的节点。
父组件中的数据发生了变化,子组件会进行 render 吗?
不一定。
-
父组件的变化与子组件无关(即:没有改变传给子组件的 props,且子组件的 tag、key 也未变):
-
父组件重新 render,生成新的 VNode 树。
-
在 patch 阶段,Vue 会对每个子节点进行 sameVnode 判断(检查类型、key、props 等核心特征)。
-
由于子组件的 VNode 描述(类型、key、props 等)未变,Vue 判定可以复用该子组件实例,不会销毁重建子组件。
-
因此:
-
子组件 不会重新执行 render 函数
-
子组件内部的 VNode 树由于新旧 VNode 完全一致,Diff 过程无实际差异对比(并非不执行 Diff 逻辑,而是 Diff 结果为 “无差异”,因此不会触发更新操作)
-
子组件生命周期钩子(如 updated)也 不会触发
-
-
-
父组件的变化影响了子组件(如:传递的 props 改变,或 key 变化):
-
父组件生成的新 VNode 中,在 patch 到该变化的子组件节点时,子组件节点的 props 或 key 发生了变化。
-
若 key 变化:
-
sameVnode 判断为 false
-
销毁旧子组件并创建新实例
-
更新视图
-
-
若只是 props 变化而 key 不变:
-
sameVnode 判断仍为 true(属于同一组件)
-
重新执行 render() → 生成新的 VNode
-
然后对子组件的内部 VNode 树进行 Diff
-
更新视图
-
-
异步更新队列
在同一事件循环中的所有数据变更只会触发一次更新,减少了 Diff 计算和 DOM 操作的次数。(通过 nextTick 机制确保更新完成后执行回调)
精确的依赖追踪
只有当组件内部真正使用的数据发生变化时,才会触发该组件的重新渲染,无关数据变化不会产生影响。
除这些之外,还有【同级比较】、【使用 key 属性识别节点身份】、【双端比较算法】,这些属于 Vue Diff 的范畴。
相信在看完以上虚拟 DOM 的相关内容后,就会对 Diff 算法有一个初步的概念:
Diff 的对象是虚拟 DOM,更新真实 DOM 是 Diff 算法的结果。
更多推荐

所有评论(0)