用Vue 3.2有一段时间了,但很多人只停留在使用层面,对底层原理一知半解。

遇到问题的时候,只能靠猜,或者Google。如果理解了底层原理,不仅能更好地使用框架,遇到问题也能快速定位,甚至能优化性能。

本文深入剖析Vue 3.2+生态的底层机制,包括响应式系统原理、虚拟DOM和Diff算法、编译器优化、组件渲染流程、状态管理原理等。

一、Vue 3的整体架构

先说说Vue 3的整体架构。

1. 核心模块

Vue 3的核心,分为几个模块:

  • 响应式系统(Reactivity):数据劫持,依赖收集,触发更新
  • 编译器(Compiler):把模板编译成渲染函数
  • 运行时(Runtime):虚拟DOM、组件系统、Diff算法
  • 服务端渲染(SSR):服务端渲染相关

这些模块是解耦的,可以独立使用。比如,你可以只用响应式系统,不用编译器和运行时。

2. 编译时和运行时

Vue 3是一个"编译时+运行时"的框架。

  • 编译时:把模板(template)编译成渲染函数(render function)。编译时做了很多优化,比如静态提升、补丁标记等。
  • 运行时:执行渲染函数,生成虚拟DOM,Diff,更新真实DOM。

这种设计,兼顾了开发体验(用模板写起来简单)和运行时性能(编译时优化)。

3. 和Vue 2的区别

Vue 3和Vue 2的核心区别:

  • 响应式:Vue 2用Object.defineProperty,Vue 3用Proxy
  • 虚拟DOM:Vue 3重写了,性能更好
  • 编译器:Vue 3的编译器做了更多优化
  • 组合式API:Vue 3新增了Composition API
  • TypeScript:Vue 3用TypeScript重写,类型支持更好

二、响应式系统原理

响应式是Vue的核心。Vue 3的响应式系统,和Vue 2有本质区别。

1. Vue 2的响应式:Object.defineProperty

Vue 2用Object.defineProperty劫持对象的属性:

Object.defineProperty(obj, 'name', {
  get() {
    // 依赖收集
    return value
  },
  set(newValue) {
    // 触发更新
    value = newValue
  }
})

这种方式的缺点:

  • 只能劫持已有的属性,新增的属性需要手动处理(Vue.set)
  • 不能劫持数组的变化(Vue 2重写了数组方法)
  • 不能劫持属性的删除(Vue.delete)
  • 性能差,需要递归遍历所有属性

2. Vue 3的响应式:Proxy

Vue 3用Proxy代理整个对象:

const proxy = new Proxy(obj, {
  get(target, key) {
    // 依赖收集
    track(target, key)
    return target[key]
  },
  set(target, key, value) {
    // 触发更新
    target[key] = value
    trigger(target, key)
    return true
  },
  deleteProperty(target, key) {
    // 处理删除
    delete target[key]
    trigger(target, key)
    return true
  }
})

Proxy的优势:

  • 可以劫持整个对象,包括新增和删除的属性
  • 可以劫持数组
  • 不需要递归遍历,性能更好
  • 可以拦截更多操作(has、ownKeys等)

3. 依赖收集和触发

Vue 3响应式的核心,是依赖收集和触发。

依赖收集(track):

  • 当渲染函数执行时,会访问响应式数据
  • 访问时,触发get拦截器
  • get拦截器调用track函数,把当前的渲染函数(effect)记录为这个属性的依赖
  • 依赖关系存在一个Map里:target -> key -> Set of effects

触发更新(trigger):

  • 当响应式数据变化时,触发set拦截器
  • set拦截器调用trigger函数
  • trigger函数找到这个属性的所有依赖(effects),依次执行
  • 执行渲染函数,重新生成虚拟DOM,更新视图

4. ref和reactive的区别

Vue 3有两种响应式API:ref和reactive。

  • reactive:用Proxy代理对象,适合对象和数组
  • ref:用一个有value属性的对象,适合基本类型

ref的本质,是一个有getter/setter的对象:

function ref(value) {
  return {
    get value() {
      track(ref, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(ref, 'value')
    }
  }
}

在模板中,ref会自动解包,所以不用写.value。但在JS中,必须用.value访问。

5. 响应式的粒度

Vue 3的响应式是"组件级"的。

也就是说,当组件中的响应式数据变化时,整个组件的渲染函数会重新执行,生成新的虚拟DOM,然后Diff更新。

不是每个属性变化都更新对应的DOM节点,而是整个组件重新渲染,然后Diff找出变化的部分。

这种设计,平衡了性能和实现复杂度。

三、虚拟DOM和Diff算法

1. 什么是虚拟DOM

虚拟DOM(Virtual DOM),是用JS对象描述真实DOM的结构。

比如:

const vnode = {
  type: 'div',
  props: { class: 'container' },
  children: [
    { type: 'p', children: 'Hello' }
  ]
}

虚拟DOM的好处:

  • 跨平台:不依赖真实DOM,可以在Node.js、小程序等环境运行
  • 性能:批量更新,减少真实DOM操作
  • 声明式:用数据描述视图,不用手动操作DOM

2. 渲染流程

Vue的渲染流程:

  1. 数据变化,触发渲染函数重新执行
  2. 渲染函数返回新的虚拟DOM树
  3. 对比新旧虚拟DOM树(Diff)
  4. 找出变化的部分,更新真实DOM

3. Diff算法

Diff算法,是对比新旧虚拟DOM树,找出最小更新量的算法。

Vue 3的Diff算法,和Vue 2相比,有很大改进:

Vue 2的Diff:

  • 双端比较:同时从新旧列表的两端开始比较
  • 时间复杂度O(n),但有一些边界情况处理不好

Vue 3的Diff:

  • 先处理相同的前缀和后缀
  • 中间部分用最长递增子序列(LIS)优化
  • 时间复杂度O(n log n),但实际性能更好

Vue 3的Diff,利用了编译器的优化信息(补丁标记),可以更快地找出变化的部分。

4. 最长递增子序列(LIS)

Vue 3 Diff的核心优化,是最长递增子序列。

简单说,在新旧节点的对比中,找出不需要移动的节点序列(最长递增子序列),只移动其他节点。这样可以减少DOM操作的次数。

比如,旧节点顺序是[A, B, C, D, E],新节点顺序是[A, C, B, E, D]。最长递增子序列是[A, C, E],这三个节点不需要移动,只需要移动B和D。

这个优化,在列表更新频繁的场景下,性能提升很明显。

四、编译器优化

Vue 3的编译器,做了很多优化,这是Vue 3性能好的重要原因。

1. 静态提升(Static Hoisting)

编译器会把模板中不变的部分(静态节点)提升到渲染函数外面,只创建一次,复用。

比如:

<div>
  <p>静态文本</p>
  <p>{{ dynamic }}</p>
</div>

编译后,<p>静态文本</p>会被提升到渲染函数外面,每次渲染只创建动态部分。

2. 补丁标记(Patch Flags)

编译器会给动态节点加上补丁标记,告诉运行时哪些部分可能变化。

比如:

<p class="red">{{ text }}</p>

编译后,这个节点会被标记为TEXT(文本可能变化),class是静态的,不用对比。

运行时Diff的时候,只对比标记为动态的部分,跳过静态部分。这大大减少了Diff的工作量。

3. 缓存事件处理函数

编译器会缓存事件处理函数,避免每次渲染都创建新的函数。

比如:

<button @click="handleClick">点击</button>

编译后,handleClick会被缓存,不会因为重新渲染而创建新的函数引用。这可以避免子组件因为props变化而不必要地重新渲染。

4. 块级优化(Block Tree)

Vue 3的编译器,会把模板分成"块"(Block),每个块有稳定的结构。

Diff的时候,只在块内对比,不用遍历整个树。这可以减少Diff的遍历次数。

五、组件渲染流程

1. 组件的挂载

组件挂载的流程:

  1. 创建组件实例
  2. 初始化props、slots、emit等
  3. 执行setup函数(Composition API)
  4. 执行渲染函数,生成虚拟DOM
  5. 把虚拟DOM转换成真实DOM,挂载到页面
  6. 执行mounted生命周期钩子

2. 组件的更新

组件更新的流程:

  1. 响应式数据变化,触发渲染函数重新执行
  2. 生成新的虚拟DOM
  3. Diff新旧虚拟DOM
  4. 更新真实DOM
  5. 执行updated生命周期钩子

3. 组件的卸载

组件卸载的流程:

  1. 执行beforeUnmount生命周期钩子
  2. 移除真实DOM
  3. 停止响应式监听
  4. 清理定时器、事件监听等
  5. 执行unmounted生命周期钩子

4. 父子组件的渲染顺序

挂载时:父组件先挂载,然后子组件挂载。但mounted钩子的执行顺序是:子组件先mounted,然后父组件mounted。

更新时:父组件先更新,然后子组件更新。但updated钩子的执行顺序是:子组件先updated,然后父组件updated。

这个顺序,有时候会导致一些问题,比如在父组件的mounted中访问子组件的ref,可能还没准备好。

六、状态管理原理(Pinia)

Vue 3推荐用Pinia做状态管理。说说Pinia的原理。

1. Pinia的核心

Pinia的核心,是Vue 3的响应式系统。

Pinia的store,本质上是一个reactive对象:

const state = reactive({ count: 0 })

getters,本质上是computed:

const doubleCount = computed(() => state.count * 2)

actions,就是普通的函数,可以修改state。

2. 为什么用Pinia而不是直接用reactive

直接用reactive也能做状态管理,但Pinia提供了更多:

  • DevTools支持:可以追踪状态变化,时间旅行
  • 插件系统:可以扩展功能(持久化、日志等)
  • 类型安全:TypeScript支持好
  • 模块化:多个store,各自独立
  • SSR支持

3. Pinia和Vuex的区别

  • Vuex有mutations,Pinia没有,直接在actions中修改state
  • Pinia更简单,API更少
  • Pinia对TypeScript支持更好
  • Pinia支持组合式API的写法

七、性能优化的原理

理解了原理,才能做好性能优化。

1. 减少响应式数据

响应式数据越多,依赖收集和触发的开销越大。

优化:

  • 不需要响应的数据,用普通对象或shallowRef
  • 大对象用shallowReactive,只做浅层响应式
  • 不需要在模板中用的数据,不要放在响应式系统里

2. 合理使用computed

computed有缓存,只有依赖变化时才重新计算。

优化:

  • 复杂的计算用computed,不要在模板中写复杂表达式
  • 避免在computed中做副作用

3. 避免不必要的组件渲染

组件重新渲染的开销不小。

优化:

  • 用v-once标记不需要更新的内容
  • 合理使用v-memo
  • 把大组件拆成小组件,减少重新渲染的范围
  • 用shallowRef或shallowReactive,减少触发更新的频率

4. 列表优化

列表是性能瓶颈的常见来源。

优化:

  • 用v-for必须加key,而且key要稳定
  • 长列表用虚拟滚动(vue-virtual-scroller)
  • 避免在v-for中使用复杂的计算

八、常见问题的原理分析

1. 为什么reactive解构后失去响应式

因为reactive是用Proxy代理的,解构后得到的是普通值,失去了Proxy的代理。

解决:用toRefs把属性转成ref,或者直接用ref。

2. 为什么ref在模板中不用.value

因为Vue的编译器在编译模板时,会自动识别ref,加上.value。这是编译时做的,运行时不需要。

3. 为什么setup中没有this

因为setup在组件实例创建之前执行,还没有this。需要用组合式函数(useRoute、useRouter等)代替。

4. 为什么Vue 3的性能更好

  • Proxy比Object.defineProperty性能好
  • 编译器优化(静态提升、补丁标记、块级优化)
  • Diff算法优化(最长递增子序列)
  • 更小的打包体积(Tree Shaking)

九、写在最后

Vue 3.2+的生态,看起来复杂,但核心原理并不难理解。

响应式系统是基础,虚拟DOM和Diff是核心,编译器优化是性能的关键,组件系统是骨架。理解了这些,就能从"会用"变成"懂原理"。

懂原理的好处是:

  • 遇到问题能快速定位,不用靠猜
  • 能写出性能更好的代码
  • 能理解框架的设计思想,甚至能参与开源贡献
  • 能更好地评估技术选型

2022年了,Vue 3已经很成熟了,生态也越来越完善。但很多人还停留在Vue 2的思维方式,或者只会用API,不懂原理。希望这篇文章,能帮你深入理解Vue 3的底层机制。

最后,用一句话总结:"框架是工具,原理是根本。会用API是基础,懂原理才能走得更远。"

愿你在Vue的世界里,不仅会用,更懂原理。