Note 4
Vue3
的状态管理 Pinia
的核心机制并非传统意义上的 发布订阅模式(Pub-Sub),而是基于 Vue3 的响应式系统(Reactivity System)实现的。以下是详细分析:
1. 核心机制:响应式系统
Pinia
的底层依赖于 Vue3
的 reactive
和 ref
等响应式 API
,其核心原理是通过 依赖追踪(Dependency Tracking) 和 触发更新(Trigger Updates) 实现状态管理。具体流程如下:
状态定义:
使用defineStore
创建 Store,内部状态(state
)会被转换为响应式对象。jsconst useStore = defineStore('storeId', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })
依赖收集:
当组件中通过useStore()
访问状态(如store.count
)时,Vue3
会通过Proxy
自动追踪这些属性的依赖关系,将当前组件注册为这些状态的“订阅者”。触发更新:
当状态变更时(如调用store.increment()
),响应式系统会通知所有依赖该状态的组件重新渲染。
2. 与传统发布订阅模式的区别
虽然 Pinia
的状态变更会触发组件更新,但其机制与典型的发布订阅模式有以下关键差异:
特性 | Pinia(响应式系统) | 发布订阅模式 |
---|---|---|
通信方式 | 隐式依赖追踪,自动绑定依赖关系 | 显式订阅事件,手动管理订阅关系 |
耦合性 | 组件与状态直接绑定,无需手动订阅 | 发布者和订阅者解耦,通过事件通道通信 |
性能优化 | 依赖 Vue 的响应式优化(如惰性更新) | 需自行实现事件过滤或节流 |
典型场景 | 状态驱动 UI 更新 | 跨组件或跨层级的松散耦合通信 |
3. 为什么 Pinia
不直接使用发布订阅?
- 与 Vue 生态深度集成:
Vue
的响应式系统天然支持自动依赖追踪,无需手动维护订阅关系,简化了开发流程。 - 性能优势:
响应式系统通过细粒度依赖追踪(如基于Proxy
的监听),仅在相关状态变化时触发更新,避免了传统Pub-Sub
中可能存在的无效通知。 - 开发体验优化:
开发者无需手动订阅/取消订阅,减少了内存泄漏风险,代码更简洁。
4. 补充:Pinia 中的“类发布订阅”行为
尽管核心机制不同,Pinia
仍提供了一些类似发布订阅的 API,用于特定场景:
store.$subscribe()
:
监听Store
中状态的变更(类似订阅全局状态变化事件)。jsstore.$subscribe((mutation, state) => { console.log('状态变更:', mutation.type, state) })
store.$onAction()
: 监听Action
的执行(类似订阅动作触发事件)。jsstore.$onAction(({ name, args, after, onError }) => { console.log('Action 触发:', name, args) })
5. 总结
Pinia
的核心机制是响应式系统,而非传统发布订阅模式。- 其优势在于 自动依赖追踪 和 与
Vue
生态的无缝集成,避免了手动管理订阅关系的复杂性。 - 在需要跨组件通信或非响应式场景时,可结合
Vue
的provide/inject
或第三方Pub-Sub
库(如mitt
)扩展功能。
虚拟 DOM
Virtual DOM
是对 DOM
的抽象,本质上是 JavaScript
对象
Virtual DOM
是一棵以 JavaScript
对象作为基础的树,每一个节点称为 VNode
,用对象属性来描述节点,实际上它是一层对真实 DOM
的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说 Virtual DOM
就是一个 JS
对象,用以描述整个文档。
Virtual DOM
的优势
- 操作
DOM
慢,JS
运行效率高,提高效率。 因为DOM
操作的执行速度远不如Javascript
的运算速度快,因此,把大量的DOM
操作搬运到JavaScript
中,运用patching
算法来计算出真正需要更新的节点,能最大限度地减少DOM
操作,从而显著提高性能。· 本质上就是在JS
和DOM
之间做了一个缓存。 - 提升渲染性能,
Virtual DOM
的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。 和DOM
操作比起来,js
计算是极其便宜的。这才是为什么要有Virtual DOM
,它保证了
- 不管你的数据变化多少,每次重绘的性能都可以接受
- 依然可以用类似
innerHTML
的思路去写你的应用
Virtual DOM
的真正价值不是性能,而在于:
- 为函数式的UI编程方式打开了大门
- 可以将
JS
对象渲染到浏览器DOM
以外的backend
(环境中),也就是支持了跨平台的开发,比如ReactNative
就是基于React
的跨平台开发框架。
Vue
中的 虚拟 DOM
在 Vue.js
中,虚拟 DOM(Virtual DOM) 是框架实现高效渲染的核心机制。它通过抽象真实 DOM
的复杂性,结合 Diff
算法和批量更新策略,显著提升了 Web
应用的性能表现。
一、虚拟 DOM 的本质
- 轻量级 JavaScript 对象 虚拟
DOM
是真实DOM
的抽象表示,本质是一个包含节点类型、属性、子节点等信息的 普通 JS 对象。例如:
const vnode = {
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'p', text: 'Hello Vue' }
]
}
- 与真实 DOM 的关键区别
特性 | 真实 DOM | 虚拟 DOM |
---|---|---|
操作代价 | 昂贵(触发重绘/回流) | 轻量(仅 JS 对象操作) |
更新策略 | 直接修改 | 批量 Diff 后更新 |
平台依赖性 | 强(浏览器环境) | 跨平台(如 SSR 、小程序) |
二、虚拟 DOM 的核心工作流程
1. 初始化阶段
模板编译:将
Vue
模板(.vue
文件或template
选项)编译为 渲染函数(render function)生成虚拟 DOM:首次渲染时,执行渲染函数生成初始虚拟
DOM
树js// 编译后的渲染函数示例 render(h) { return h('div', { id: 'app' }, this.message) }
2. 更新阶段
- 数据变更触发:当响应式数据变化时,重新执行渲染函数生成 新虚拟 DOM 树
Diff
算法(差异对比):对比新旧虚拟DOM
树,找出最小变更集(同级比较、Key
优化等)- 批量更新真实
DOM
:通过 patch 函数 将差异应用到真实DOM
,避免频繁重绘/回流
三、Diff
算法的核心优化策略
Vue
的 Diff
算法通过以下策略减少计算量(时间复杂度 O(n)):
- 同层级比较
- 仅比较同一层级的节点,不跨层级移动(减少递归深度)
- Key 的作用
- 通过唯一
key
标识节点身份,复用相同Key
的DOM
元素(避免不必要的销毁/重建)
<!-- 列表渲染时推荐使用唯一 Key -->
<div v-for="item in list" :key="item.id">{{ item.text }}</div>
- 双端指针优化
- 在新旧子节点数组的首尾设置指针,优先处理头尾相同节点(减少遍历次数)
四、虚拟 DOM 的性能优势与代价
优势
- 减少直接 DOM 操作 批量合并多次数据变更,避免频繁触发浏览器渲染机制(如 100 次数据变化 → 1 次 DOM 更新)。
- 跨平台能力 虚拟
DOM
抽象了平台差异,使得Vue
可适配不同渲染目标(如Web
、Native
、Canvas
)。 - 开发体验优化 开发者无需手动优化
DOM
操作,专注于数据逻辑。
代价
- 内存占用 需额外存储虚拟
DOM
树,对低性能设备可能产生压力。 - 初始渲染开销
首次渲染需经过模板编译 →虚拟 DOM
→真实 DOM
的转换流程,比纯字符串拼接稍慢。 - 极端场景性能瓶颈
超大规模动态列表或高频更新场景(如股票行情),需手动优化(如虚拟滚动)。
五、虚拟 DOM 在 Vue 3 中的优化
Vue 3
对虚拟 DOM
进行了多项改进:
- 静态提升(Static Hoisting)
- 将静态节点提取到渲染函数外部,避免重复创建:
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "Static Content", -1 /* HOISTED */)
- Patch Flag 标记
- 在虚拟 DOM 节点中标记动态属性类型(如
class
、style
),Diff 时跳过静态内容:
{ tag: 'div', patchFlag: 1 } // 1 表示只有文本内容是动态的
- 缓存事件处理函数
- 避免每次渲染重新生成事件回调,减少不必要的更新。
六、虚拟 DOM 应用场景与最佳实践
适用场景
- 中大型动态应用(频繁数据更新)
- 跨平台开发(如
Vue Native
、SSR
) - 需要复杂交互的组件(如表单、拖拽)
规避性能陷阱
- 避免滥用大型虚拟 DOM 树
- 使用
v-show
替代v-if
控制频繁切换的组件 - 拆分复杂组件为多个子组件(利用局部更新特性)
- 合理使用 Key
- 列表项必须设置唯一且稳定的
Key
,避免使用索引
<!-- 错误示例 -->
<div v-for="(item, index) in list" :key="index">
- 手动优化高频更新
- 使用
v-once
标记静态内容 - 对动画场景使用
CSS3
或WebGL
直接操作DOM
总结
虚拟 DOM
是 Vue
实现高效渲染的基石,通过 Diff
算法和批量更新策略,在开发便利性与性能之间取得了平衡。理解其工作原理(尤其是 Key
机制和 Diff
策略)能帮助开发者编写更高效的 Vue
代码。对于特殊性能场景,可结合 Vue
提供的优化工具(如 v-memo
、<KeepAlive>
)或直接操作 DOM
进行针对性优化。
Vue2
和 Vue3
的 Diff
算法区别
Vue2
和 Vue3
的 Diff
算法在核心策略和性能优化上存在显著差异,这些改进使得 Vue3
在处理复杂场景时更高效。
1. 核心算法策略
- Vue2:采用双端比较(双指针)算法,通过头尾四个指针(
oldStart
、oldEnd
、newStart
、newEnd
)逐层比较新旧子节点列表,尝试复用节点并减少移动次数。若无法匹配,则通过遍历查找可复用节点,时间复杂度为 O(n³) 。 - Vue3:引入动态规划与最长递增子序列 LIS优化,先处理相同的前/后置节点,再对剩余节点构建映射表并计算最长递增子序列,从而减少节点移动次数。时间复杂度优化至 O(n²) 或更低。
2. 静态内容处理
- Vue2:静态节点在首次渲染后仍需参与
Diff
比较,无法完全跳过更新,导致冗余计算。 - Vue3:通过静态提升(Hoisting)将模板中的静态节点提取到外层作用域,后续更新时直接复用,无需比较。此外,引入块树 Block Tree概念,跳过静态块的
Diff
过程,大幅减少计算量。
3. 动态节点优化
- Vue2:动态节点的更新需全量比较属性、事件和子节点,即使仅部分内容变化。
- Vue3:通过PatchFlag 标记动态节点,例如文本变化、属性更新等。
Diff
时仅检查标记的字段,跳过无关属性的比较。例如,仅文本变化的节点会标记为TEXT
类型,无需遍历属性。
4. 数据结构支持
- Vue2:不支持多根节点(Fragment),每个组件必须包裹在单一根节点中。
- Vue3:原生支持Fragment(多根节点)和Teleport(跨
DOM
渲染),Diff
算法针对这些结构进行了优化,减少层级嵌套带来的性能损耗。
5. 列表 Diff 的改进
- Vue2:在处理乱序列表时,可能因频繁移动节点导致性能下降,尤其在无
key
时采用就地复用策略,易引发渲染错误。 - Vue3:结合
key
值和位置映射表,通过LIS
算法确定最少移动次数。例如,若新列表顺序为b, c, a
,Vue3
会优先复用旧节点并按需移动,而非全量重建。
6. 编译时优化
Vue3
的编译器在构建阶段分析模板,提取动态部分并生成优化后的虚拟DOM
结构。例如:- 缓存事件处理函数,避免重复创建。
- 静态属性提升,将不变的属性直接绑定到外层,减少
Diff
范围。
性能对比
- 基准测试:
Vue3
的渲染性能比Vue2
提升约100%-200%
,尤其在大型应用中表现更优。 - 场景示例:对于包含大量动态列表的组件,
Vue3
的PatchFlag
和LIS
优化可减少50%
以上的DOM
操作。
总结
Vue3
的 Diff
算法通过静态提升、PatchFlag、LIS 优化等策略,显著减少了无效计算和 DOM 操作,适用于高复杂度场景。而 Vue2
的双端比较虽能满足一般需求,但在处理动态内容时性能较弱。升级到 Vue3
可显著提升应用的响应速度和渲染效率。
Vue 中父子组件的生命周期执行顺序
在 Vue2
和 Vue3
中,父子组件的生命周期执行顺序基本一致,主要区别在于部分钩子名称的变化(如卸载阶段的钩子)。
Vue2
生命周期执行顺序
加载阶段
- 父组件:
beforeCreate
→created
→beforeMount
- 子组件:
beforeCreate
→created
→beforeMount
→mounted
- 父组件:
mounted
- 父组件:
更新阶段(父组件数据变化触发子组件更新时)
- 父组件:
beforeUpdate
- 子组件:
beforeUpdate
→updated
- 父组件:
updated
- 父组件:
卸载阶段
- 父组件:
beforeDestroy
- 子组件:
beforeDestroy
→destroyed
- 父组件:
destroyed
- 父组件:
Vue3 生命周期执行顺序
加载阶段
- 父组件:
beforeCreate
→created
→beforeMount
- 子组件:
beforeCreate
→created
→beforeMount
→mounted
- 父组件:
mounted
(注意:
Vue3
中可通过setup()
替代beforeCreate
和created
,但执行顺序不变)- 父组件:
更新阶段
- 父组件:
beforeUpdate
- 子组件:
beforeUpdate
→updated
- 父组件:
updated
- 父组件:
卸载阶段
- 父组件:
beforeUnmount
- 子组件:
beforeUnmount
→unmounted
- 父组件:
unmounted
- 父组件:
关键区别
钩子名称变化
Vue2
的beforeDestroy
和destroyed
在Vue3
中更名为beforeUnmount
和unmounted
。
Composition API 的影响
Vue3
的setup()
函数在beforeCreate
之前执行,但不影响父子组件的执行顺序逻辑。
总结
- 挂载顺序:父组件初始化 → 子组件挂载 → 父组件完成挂载。
- 更新顺序:父组件触发更新 → 子组件更新 → 父组件完成更新。
- 卸载顺序:父组件开始卸载 → 子组件卸载 → 父组件完成卸载。
Vue2
和 Vue3
的差异仅体现在钩子命名上,核心执行逻辑保持一致。