文章目录
- [一、 Vue 虚拟 DOM (VDOM) 深度解析](#一、 Vue 虚拟 DOM (VDOM) 深度解析)
-
- [1. 什么是虚拟DOM](#1. 什么是虚拟DOM)
- [2. 为什么需要虚拟 DOM?](#2. 为什么需要虚拟 DOM?)
- [3. VDOM 是如何生成的?](#3. VDOM 是如何生成的?)
- [4. VDOM 如何做 Diff 算法?](#4. VDOM 如何做 Diff 算法?)
-
- [A 核心步骤:Patch 过程](#A 核心步骤:Patch 过程)
- [B 为什么 key 很重要?](#B 为什么 key 很重要?)
- [二 、 既然 vue 通过数据劫持可以精确探测数据变化,为什么还需要虚拟 DOM进行 diff 算法?](#二 、 既然 vue 通过数据劫持可以精确探测数据变化,为什么还需要虚拟 DOM进行 diff 算法?)
-
-
- [1. 性能与内存的权衡(颗粒度问题)](#1. 性能与内存的权衡(颗粒度问题))
- [2. 跨平台能力(Abstraction)](#2. 跨平台能力(Abstraction))
- [3. 保证更新的"确定性"与批量处理](#3. 保证更新的“确定性”与批量处理)
-
一、 Vue 虚拟 DOM (VDOM) 深度解析
1. 什么是虚拟DOM
虚拟 DOM 本质上是一个 JavaScript 对象,它用简单的属性来描述真实 DOM 的结构。
Vue 使用虚拟 DOM 主要是为了在 高性能 和 开发体验 之间取得平衡。虽然直接操作原生 DOM 最快,但在复杂的应用中,手动优化每一个 DOM 节点既困难又容易出错。
VNode 结构示例
javascript
{
tag: 'div', // 标签名(如 div, span)或组件名。
props: { id: 'app' }, // 属性(如 id, class, style)。
children: [
{ tag: 'p', children: 'Hello World' }
], // 子节点(可以是文本,也可以是另一个 VNode 数组)
key: 123 // 用于优化 diff 的唯一标识
}
2. 为什么需要虚拟 DOM?
很多人误以为 VDOM 比原生 DOM 快,其实这是一个误区。VDOM 的真正价值在于:
-
减少无谓的重绘与回流: 直接操作 DOM 是很昂贵的。如果你在循环中多次修改 DOM,浏览器会进行多次重排。VDOM 允许 Vue 先在内存中计算好最终的结构,然后一次性"同步"给真实 DOM。
-
跨平台能力: 因为 VDOM 是一个普通的 JavaScript 对象,它不仅可以映射到浏览器的 DOM,还可以映射到小程序、Weex 或移动端原生组件(iOS/Android)。
-
声明式编程: 开发者只需要关心数据的状态(State),而不需要手动去写 appendChild 或 removeChild。Vue 会自动通过 VDOM 找出状态变化前后的差异并更新。
3. VDOM 是如何生成的?
在 Vue 中,VDOM 是通过 渲染函数(Render Function) 生成的。
-
模板编译 : 你写的
<template>标签会被 Vue 的编译器转换成 render 函数。 -
执行渲染函数: 当组件需要渲染或数据发生变化时,Vue 会执行这个 render 函数。
-
产生 VNode: render 函数内部会调用类似 h() (hyperscript) 的方法。这个函数会返回一个纯 JavaScript 对象,这就是 VNode(虚拟节点)。
4. VDOM 如何做 Diff 算法?
vue 的 Diff 算法遵循 "深度优先、同层比较" 的策略。
-
深度优先 (Depth-First Search, DFS): 当比较两棵虚拟 DOM 树时,算法会先沿着一个节点往下走,直到最底层的子节点,然后再回过头处理旁边的兄弟节点。
-
执行逻辑:如果发现
<div>标签没变,它不会立刻去看<div>后面的<p>标签,而是先钻进<div>内部去对比它的 children(子节点)。 -
为什么要这么做? DOM 是树状结构。为了确保父节点及其包含的所有子节点都能正确更新,Vue 会递归地遍历每一个分支,确保局部结构的完整性。
-
-
同层比较:算法只会对 同一层级 的节点进行比较,而不会跨层级寻找。
- 核心规则:如果旧树的第三层有一个
<span>,新树把它移到了第二层,Vue 不会去尝试复用这个 。它会直接销毁旧的 ,并在新位置创建一个新的。 - 为什么不跨层找? 在 Web 开发中,DOM 节点的跨层级移动其实非常罕见。如果要进行全量对比(即 O ( n 3 ) O(n^3) O(n3) 复杂度的算法),性能消耗巨大。Vue 通过"同层比较"将复杂度降低到了 O ( n ) O(n) O(n),极大地提升了速度。
- 核心规则:如果旧树的第三层有一个
A 核心步骤:Patch 过程
当数据变化导致生成了新的 VNode 时,Vue 会将新旧两棵 VNode 树进行对比:
-
同层比较: 如果新旧节点的父节点不同,直接销毁旧的,创建新的。
-
判断是否为相同节点 : Vue 通过 tag 和 key 来判断两个节点是否是"同一个"。如果不同,直接替换。
-
对比子节点 (Children): 这是 Diff 的精华所在
-
Vue 2:双端 Diff 算法
-
Vue 3:快速 Diff 算法 (Quick Diff)
-
Vue2的 双端 diff算法 与 Vue3 的 快速diff 算法
B 为什么 key 很重要?
在 Diff 过程中,key 是节点的唯一身份证。有了 key,Vue 就能准确知道某个节点只是移动了位置,而不是被删除了,从而避免不必要的 DOM 重新创建,极大提升了列表渲染的性能。
二 、 既然 vue 通过数据劫持可以精确探测数据变化,为什么还需要虚拟 DOM进行 diff 算法?
简单来说:数据劫持(Proxy/Object.defineProperty)能让你知道"谁变了",但不能高效地告诉你"怎么变最省钱"。
我们可以从以下三个维度来构建面试答案:
1. 性能与内存的权衡(颗粒度问题)
Vue 的响应式系统确实可以做到"极致精确",比如给每个 HTML 标签甚至每个文本节点都绑定一个 Watcher/Effect。但这样做会有严重的副作用:
- **内存开销巨大**:如果页面有 10,000 个动态节点,就会有 10,000 个 Effect 对象。在大型应用中,这会吃掉大量内存。
- **依赖追踪的成本**:每个节点都去收集依赖、建立映射关系,初始化的耗时会非常长。
Vue 的方案:采取了中等颗粒度。
- 每个组件对应一个 Watcher/Effect。当组件内的数据变了,Vue 只知道"这个组件需要重新渲染",但不知道组件内部到底是哪一个
<div>变了。 - 这时候,就需要 虚拟 DOM 和 Diff 算法 来找出组件内部最小的更新范围。
2. 跨平台能力(Abstraction)
虚拟 DOM 是对真实 UI 的一层抽象。
-
如果 Vue 直接操作真实 DOM,那么它就只能运行在浏览器里。
-
有了虚拟 DOM,渲染函数产出的是一个 JavaScript 对象。这个对象可以被渲染成浏览器里的 DOM,也可以通过不同的渲染器(Renderer)变成 iOS/Android 的 原生控件(如 Weex),甚至是 Canvas 或 SSR(服务端字符串)。
如果没有虚拟 DOM 这一层中间层,Vue 的跨平台扩展性会大打折扣。
3. 保证更新的"确定性"与批量处理
在复杂的业务逻辑中,你可能会在一行代码里连续修改 5 个状态变量。
- 如果没有虚拟 DOM:可能触发 5 次真实的 DOM 操作,引发 5 次严重的浏览器重排(Reflow)。
有了虚拟 DOM:
-
5 次修改触发 1 次组件 Effect 运行(异步队列机制)。
-
渲染函数生成一套新的虚拟 DOM 树。
-
Diff 算法 在内存里把这 5 次修改合并,最终计算出一次最精简的 DOM 指令。
总结:
-
"Vue 之所以保留虚拟 DOM,是为了在响应式追踪的内存开销和DOM 操作的执行效率之间寻找最优解。
-
响应式系统解决了 '何时更新'(Scheduling)的问题,而虚拟 DOM 和 Diff 算法解决了'如何最小化更新'(Rendering) 的问题。
-
到了 Vue 3,通过 Compiler-Informed Virtual DOM(带有编译时信息的虚拟 DOM),Vue 进一步在 Diff 阶段利用 静态标记(Patch Flags) 跳过了不动的节点,让 Diff 的性能趋近于原生操作。"