Skip to content

Note 4

Vue3 的状态管理 Pinia

的核心机制并非传统意义上的 发布订阅模式(Pub-Sub),而是基于 Vue3 的响应式系统(Reactivity System)实现的。以下是详细分析:

1. 核心机制:响应式系统


Pinia 的底层依赖于 Vue3reactiveref 等响应式 API,其核心原理是通过 依赖追踪(Dependency Tracking)触发更新(Trigger Updates) 实现状态管理。具体流程如下:

  1. 状态定义
    使用 defineStore 创建 Store,内部状态(state)会被转换为响应式对象。

    js
    const useStore = defineStore('storeId', {
      state: () => ({ count: 0 }),
      actions: {
        increment() { this.count++ }
      }
    })
  2. 依赖收集
    当组件中通过 useStore() 访问状态(如 store.count)时,Vue3 会通过 Proxy 自动追踪这些属性的依赖关系,将当前组件注册为这些状态的“订阅者”。

  3. 触发更新
    当状态变更时(如调用 store.increment()),响应式系统会通知所有依赖该状态的组件重新渲染。

2. 与传统发布订阅模式的区别


虽然 Pinia 的状态变更会触发组件更新,但其机制与典型的发布订阅模式有以下关键差异:

特性Pinia(响应式系统)发布订阅模式
通信方式隐式依赖追踪,自动绑定依赖关系显式订阅事件,手动管理订阅关系
耦合性组件与状态直接绑定,无需手动订阅发布者和订阅者解耦,通过事件通道通信
性能优化依赖 Vue 的响应式优化(如惰性更新)需自行实现事件过滤或节流
典型场景状态驱动 UI 更新跨组件或跨层级的松散耦合通信

3. 为什么 Pinia 不直接使用发布订阅?

  1. 与 Vue 生态深度集成
    Vue 的响应式系统天然支持自动依赖追踪,无需手动维护订阅关系,简化了开发流程。
  2. 性能优势
    响应式系统通过细粒度依赖追踪(如基于 Proxy 的监听),仅在相关状态变化时触发更新,避免了传统 Pub-Sub 中可能存在的无效通知。
  3. 开发体验优化
    开发者无需手动订阅/取消订阅,减少了内存泄漏风险,代码更简洁。

4. 补充:Pinia 中的“类发布订阅”行为


尽管核心机制不同,Pinia 仍提供了一些类似发布订阅的 API,用于特定场景:

  1. store.$subscribe()
    监听 Store 中状态的变更(类似订阅全局状态变化事件)。

    js
    store.$subscribe((mutation, state) => {
      console.log('状态变更:', mutation.type, state)
    })
  2. store.$onAction(): 监听 Action 的执行(类似订阅动作触发事件)。

    js
    store.$onAction(({ name, args, after, onError }) => {
      console.log('Action 触发:', name, args)
    })

5. 总结

  • Pinia 的核心机制是响应式系统,而非传统发布订阅模式。
  • 其优势在于 自动依赖追踪Vue 生态的无缝集成,避免了手动管理订阅关系的复杂性。
  • 在需要跨组件通信或非响应式场景时,可结合 Vueprovide/inject 或第三方 Pub-Sub 库(如 mitt)扩展功能。

虚拟 DOM

Virtual DOM 是对 DOM 的抽象,本质上是 JavaScript 对象

Virtual DOM 是一棵以 JavaScript 对象作为基础的树,每一个节点称为 VNode,用对象属性来描述节点,实际上它是一层对真实 DOM 的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说 Virtual DOM 就是一个 JS 对象,用以描述整个文档。

Virtual DOM 的优势

  1. 操作 DOM 慢,JS 运行效率高,提高效率。 因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因此,把大量的 DOM 操作搬运到 JavaScript 中,运用 patching 算法来计算出真正需要更新的节点,能最大限度地减少 DOM 操作,从而显著提高性能。· 本质上就是在 JSDOM 之间做了一个缓存。
  2. 提升渲染性能,Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。 和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM,它保证了
  • 不管你的数据变化多少,每次重绘的性能都可以接受
  • 依然可以用类似 innerHTML 的思路去写你的应用

Virtual DOM 的真正价值不是性能,而在于:

  1. 为函数式的UI编程方式打开了大门
  2. 可以将 JS 对象渲染到浏览器 DOM 以外的 backend(环境中),也就是支持了跨平台的开发,比如 ReactNative 就是基于 React 的跨平台开发框架。

Vue 中的 虚拟 DOM

Vue.js 中,虚拟 DOM(Virtual DOM) 是框架实现高效渲染的核心机制。它通过抽象真实 DOM 的复杂性,结合 Diff 算法和批量更新策略,显著提升了 Web 应用的性能表现。

一、虚拟 DOM 的本质

  1. 轻量级 JavaScript 对象 虚拟 DOM 是真实 DOM 的抽象表示,本质是一个包含节点类型、属性、子节点等信息的 普通 JS 对象。例如:
js
const vnode = {
  tag: 'div',
  props: { id: 'app', class: 'container' },
  children: [
    { tag: 'p', text: 'Hello Vue' }
  ]
}
  1. 与真实 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 算法的核心优化策略


VueDiff 算法通过以下策略减少计算量(时间复杂度 O(n)):

  1. 同层级比较
  • 仅比较同一层级的节点,不跨层级移动(减少递归深度)
  1. Key 的作用
  • 通过唯一 key 标识节点身份,复用相同 KeyDOM 元素(避免不必要的销毁/重建)
html
<!-- 列表渲染时推荐使用唯一 Key -->
<div v-for="item in list" :key="item.id">{{ item.text }}</div>
  1. 双端指针优化
  • 在新旧子节点数组的首尾设置指针,优先处理头尾相同节点(减少遍历次数)

四、虚拟 DOM 的性能优势与代价

优势

  1. 减少直接 DOM 操作 批量合并多次数据变更,避免频繁触发浏览器渲染机制(如 100 次数据变化 → 1 次 DOM 更新)。
  2. 跨平台能力 虚拟 DOM 抽象了平台差异,使得 Vue 可适配不同渲染目标(如 WebNativeCanvas)。
  3. 开发体验优化 开发者无需手动优化 DOM 操作,专注于数据逻辑。

代价

  1. 内存占用 需额外存储虚拟 DOM 树,对低性能设备可能产生压力。
  2. 初始渲染开销
    首次渲染需经过模板编译 → 虚拟 DOM真实 DOM 的转换流程,比纯字符串拼接稍慢。
  3. 极端场景性能瓶颈
    超大规模动态列表或高频更新场景(如股票行情),需手动优化(如虚拟滚动)。

五、虚拟 DOM 在 Vue 3 中的优化

Vue 3 对虚拟 DOM 进行了多项改进:

  1. 静态提升(Static Hoisting)
  • 将静态节点提取到渲染函数外部,避免重复创建:
js
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "Static Content", -1 /* HOISTED */)
  1. Patch Flag 标记
  • 在虚拟 DOM 节点中标记动态属性类型(如 classstyle),Diff 时跳过静态内容:
js
{ tag: 'div', patchFlag: 1 } // 1 表示只有文本内容是动态的
  1. 缓存事件处理函数
  • 避免每次渲染重新生成事件回调,减少不必要的更新。

六、虚拟 DOM 应用场景与最佳实践

适用场景

  • 中大型动态应用(频繁数据更新)
  • 跨平台开发(如 Vue NativeSSR
  • 需要复杂交互的组件(如表单、拖拽)

规避性能陷阱

  1. 避免滥用大型虚拟 DOM 树
  • 使用 v-show 替代 v-if 控制频繁切换的组件
  • 拆分复杂组件为多个子组件(利用局部更新特性)
  1. 合理使用 Key
  • 列表项必须设置唯一且稳定的 Key,避免使用索引
vue
<!-- 错误示例 -->
<div v-for="(item, index) in list" :key="index">
  1. 手动优化高频更新
  • 使用 v-once 标记静态内容
  • 对动画场景使用 CSS3WebGL 直接操作 DOM

总结

虚拟 DOMVue 实现高效渲染的基石,通过 Diff 算法和批量更新策略,在开发便利性与性能之间取得了平衡。理解其工作原理(尤其是 Key 机制和 Diff 策略)能帮助开发者编写更高效的 Vue 代码。对于特殊性能场景,可结合 Vue 提供的优化工具(如 v-memo<KeepAlive>)或直接操作 DOM 进行针对性优化。

Vue2Vue3Diff 算法区别

Vue2Vue3Diff 算法在核心策略和性能优化上存在显著差异,这些改进使得 Vue3 在处理复杂场景时更高效。

1. 核心算法策略

  • Vue2:采用双端比较(双指针)算法,通过头尾四个指针(oldStartoldEndnewStartnewEnd)逐层比较新旧子节点列表,尝试复用节点并减少移动次数。若无法匹配,则通过遍历查找可复用节点,时间复杂度为 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, aVue3 会优先复用旧节点并按需移动,而非全量重建。

6. 编译时优化

  • Vue3 的编译器在构建阶段分析模板,提取动态部分并生成优化后的虚拟 DOM 结构。例如:
    • 缓存事件处理函数,避免重复创建。
    • 静态属性提升,将不变的属性直接绑定到外层,减少 Diff 范围。

性能对比

  • 基准测试Vue3 的渲染性能比 Vue2 提升约 100%-200%,尤其在大型应用中表现更优。
  • 场景示例:对于包含大量动态列表的组件,Vue3PatchFlagLIS 优化可减少 50% 以上的 DOM 操作。

总结

Vue3Diff 算法通过静态提升、PatchFlag、LIS 优化等策略,显著减少了无效计算和 DOM 操作,适用于高复杂度场景。而 Vue2 的双端比较虽能满足一般需求,但在处理动态内容时性能较弱。升级到 Vue3 可显著提升应用的响应速度和渲染效率。

Vue 中父子组件的生命周期执行顺序

Vue2Vue3 中,父子组件的生命周期执行顺序基本一致,主要区别在于部分钩子名称的变化(如卸载阶段的钩子)。

Vue2 生命周期执行顺序

  1. 加载阶段

    • 父组件:beforeCreatecreatedbeforeMount
    • 子组件:beforeCreatecreatedbeforeMountmounted
    • 父组件:mounted
  2. 更新阶段(父组件数据变化触发子组件更新时)

    • 父组件:beforeUpdate
    • 子组件:beforeUpdateupdated
    • 父组件:updated
  3. 卸载阶段

    • 父组件:beforeDestroy
    • 子组件:beforeDestroydestroyed
    • 父组件:destroyed

Vue3 生命周期执行顺序

  1. 加载阶段

    • 父组件:beforeCreatecreatedbeforeMount
    • 子组件:beforeCreatecreatedbeforeMountmounted
    • 父组件:mounted

    (注意:Vue3中可通过setup()替代beforeCreatecreated,但执行顺序不变)

  2. 更新阶段

    • 父组件:beforeUpdate
    • 子组件:beforeUpdateupdated
    • 父组件:updated
  3. 卸载阶段

    • 父组件:beforeUnmount
    • 子组件:beforeUnmountunmounted
    • 父组件:unmounted

关键区别

  1. 钩子名称变化

    • Vue2beforeDestroydestroyedVue3 中更名为 beforeUnmountunmounted
  2. Composition API 的影响

    • Vue3setup() 函数在 beforeCreate 之前执行,但不影响父子组件的执行顺序逻辑。

总结

  • 挂载顺序:父组件初始化 → 子组件挂载 → 父组件完成挂载。
  • 更新顺序:父组件触发更新 → 子组件更新 → 父组件完成更新。
  • 卸载顺序:父组件开始卸载 → 子组件卸载 → 父组件完成卸载。

Vue2Vue3 的差异仅体现在钩子命名上,核心执行逻辑保持一致。

Released under the MIT License.