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操作。
详细对比分析
Vue 的 Diff 算法是其虚拟 DOM 实现的核心,Vue 3 在这方面做了显著的优化。下面这个表格汇总了它们在核心机制和优化策略上的主要区别:
| 特性维度 | Vue 2 🟢 | Vue 3 🔵 |
|---|---|---|
| 核心算法 | 双端比较 :通过四个指针(旧前、旧后、新前、新后)进行比较 | 增强的双端比较 + 最长递增子序列 :预处理后,利用最长递增子序列算法最小化移动操作 |
| 优化策略 | 全量比较 :无论节点是否变化,每次更新都会进行创建和比较 | 编译时优化 :在编译阶段标记动态内容,实现静态提升、Patch Flag、事件缓存等 |
| PatchFlag | 无 | 有 :在虚拟DOM节点上标记动态绑定的类型,实现精准定点更新 |
| 静态提升 | 无 | 有 :将静态节点提升到渲染函数之外,只创建一次,多次复用 |
| 事件缓存 | 无 | 有 :缓存不会变化的事件处理器,避免不必要的更新检查 |
| Block Tree | 无 | 有 :将动态节点组织为树结构,减少比较范围 |
| Fragment支持 | 不支持(单根节点) | 支持(多根节点),减少不必要的包装元素 |
🔎 核心算法原理与差异
Vue 2 的双端 Diff 算法 像是从列表的两端向中间进行比对 。它设置了四个指针(旧前、旧后、新前、新后),通过四种假设性的对比(如旧前对新前、旧后对新后等)来尝试查找可复用的节点 。这种方式在常见的操作,比如头尾部的增删中,表现不错。但它的核心局限在于,它采用的是一种 "全量比较" 的策略 ,即使模板中大部分内容是静态的,每次更新时仍然需要遍历和比较所有的虚拟 DOM 节点。
Vue 3 的增强 Diff 算法 则更加智能化,它结合了编译时优化和更高效的运行时算法:
Patch Flag:精准的更新提示
这是Vue 3的一项关键优化。在编译阶段,Vue 3会分析模板,并在包含动态绑定的虚拟DOM节点上打上一个名为PatchFlag的数字标记 。这个标记指明了这个节点具体哪些内容是动态的,比如是文本、class还是props。这样,在后续的Diff过程中,算法可以直接跳过那些完全没有标记的静态节点,只关注这些带有标记的动态节点,并且只更新标记所对应的特定内容 。最长递增子序列:最小化节点移动
在处理列表更新,特别是节点顺序发生变化时,Vue 3会使用 "最长递增子序列" 算法来寻找新旧子节点列表中相对位置没有发生变化的最长节点序列 。找到这个序列后,Vue 3就知道了哪些节点是稳定不需要移动的,然后只会去移动那些不在这个序列中的、真正需要移动的节点 。这极大地减少了DOM移动操作次数。有测试表明,在处理列表乱序或完全反转等场景时,Vue 3因移动节点次数更少,性能表现优于Vue 2和React。
💎 性能影响与升级价值
Vue 3 在 Diff 算法上的这一系列改进,使得它在处理大型应用或频繁更新的列表时,性能通常优于 Vue 2 。这主要得益于其减少了不必要的虚拟节点比较和 DOM 操作次数 ,并且优化了内存使用 。
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 的差异仅体现在钩子命名上,核心执行逻辑保持一致。