在技术面试中,“Vue 2 和 Vue 3 的区别”堪称一道经典题。初级开发者可能只会背诵标准答案:“Vue 3 更快、更小、用了 Proxy。”但高级开发者会深入底层,探讨“懒代理如何优化初始化性能”、“Block Tree如何把Diff复杂度降为线性”、“WeakMap如何管理依赖”。
今天,我们就深入源码与架构层面,用原理和代码,透彻解析Vue的这场重大升级。
01 响应式原理的降维打击:从DefineProperty到Proxy
这是最核心的底层重构,直接决定了框架的响应能力和性能上限。
Vue 2:基于 Object.defineProperty 的递归劫持
Vue 2 在初始化(initState)阶段,必须对 data 返回的对象进行全量递归遍历,为每个属性设置 getter/setter。
// Vue 2 响应式简易实现
function observe(obj) {
if (!obj || typeof obj !== 'object') return;
// 🔴 痛点1:初始化时就必须递归遍历所有属性
// 如果对象层级深(如 hugeData),这里会造成严重的阻塞
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
function defineReactive(obj, key, val) {
observe(val); // 递归子属性
Object.defineProperty(obj, key, {
get() {
// 收集依赖 (Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
observe(newVal); // 新值也要递归监听
val = newVal;
// 触发更新 (notify)
}
}
});
}
Vue 2 响应式的三个主要缺陷:
- 初始化慢:面对深层嵌套的大数据对象,递归遍历耗时严重。
- 拦截盲区:无法检测对象属性的新增与删除(必须使用
Vue.set / Vue.delete)。
- 数组支持差:无法直接通过索引拦截数组变化,需重写
push, pop 等7个原型方法。
Vue 3:基于 Proxy 的懒代理
Vue.js 3 采用了 ES6 的 Proxy,配合 Reflect,实现了真正的对象拦截。
// Vue 3 响应式简易实现
function reactive(target) {
// 🟢 优势1:不需要递归!直接返回 Proxy 代理对象
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 收集依赖 (track)
// 🟢 优势2:懒代理 (Lazy Reactive)
// 只有当你真的访问到深层属性时(res),才去递归变成响应式
if (isObject(res)) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
// 触发更新 (trigger)
// 🟢 优势3:自动感知属性新增,无需 $set
return res;
}
});
}
关键改进点:
- 性能:初始化变快,只有实际被访问的嵌套属性才会被转换为响应式(懒代理)。
- 能力:可以拦截包括属性增删、数组索引修改、
length 变化在内的所有操作。
- 依赖存储:Vue 3 使用全局的
WeakMap<Target, Map<Key, Set<Effect>>> 结构存储依赖关系,替代了 Vue 2 中与属性闭包在一起的 Dep 类,更有利于垃圾回收。
02 编译优化:Diff算法的质变
当被问到“Vue 3 的 Diff 算法快在哪里?”,不要只说“快”,要深入“静态标记(PatchFlags)”和“Block Tree”。
Vue 2:全量 Diff (O(n))
Vue 2 的 Virtual DOM 是静态和动态节点混杂的。数据变化时,必须遍历整棵虚拟DOM树进行对比,即使其中大部分节点(如静态文本)永远不会变化。
// Vue 2 渲染函数(简化)
function render() {
return h('div', [
h('p', '静态文本'), // 依然会参与 Diff 比较
h('p', this.msg)
])
}
Vue 3:静态标记 + Block Tree
Vue 3 在编译阶段就进行了静态分析,区分出动态与静态内容。
模板示例:
<div id="app">
<p>静态文本</p>
<p :class="active">{{ msg }}</p>
</div>
Vue 3 编译后的渲染函数:
import { createVNode, toDisplayString, openBlock, createBlock } from 'vue'
export function render(_ctx) {
return (openBlock(), createBlock('div', null, [
// 静态节点被提升 (Hoisted),不参与 Diff
_hoisted_1,
// 动态节点:打上 PatchFlag
// 1 (TEXT) + 2 (CLASS) = 3
createVNode('p', { class: _ctx.active }, toDisplayString(_ctx.msg), 3 /* TEXT, CLASS */)
]))
}
优化的核心机制:
- PatchFlags:使用位运算(如
flag & 1 判断文本是否更新)标记动态节点的类型。Diff时,只需对比这些被标记的部分。
- Block Tree:将模板中的动态节点收集到一个扁平的数组(
dynamicChildren)中。执行Diff时,直接遍历这个动态节点数组,完全跳过所有静态节点。
- Diff策略升级:在对比可能移动的子节点列表时,Vue 3 采用快速Diff算法,并寻找最长递增子序列(LIS),以最小化DOM移动操作。相比Vue 2的“双端比较”算法,性能更高。
03 逻辑复用:Composition API 的架构意义
这不仅仅是写法的变化,更是内存与架构的优化。
场景:一个组件包含多个独立的功能逻辑(A,B,C)。
-
Vue 2 (Options API):
- 所有逻辑(
data, methods, computed)都分散在选项对象的各个属性中,并通过 this 访问。
- 逻辑复用依赖 Mixin,易导致命名冲突、数据来源不清晰。
-
Vue 3 (Composition API):
- 在
<script setup> 中,代码本质上是普通JavaScript函数的顺序执行。
- Tree Shaking友好:如果未使用
computed 等API,其代码不会被打包。Vue 2 的Options API无法做到。
- Hooks模式:可以像编写React Hooks一样,将逻辑封装为可复用的组合式函数(如
useMouse, useFetch),具备更好的类型安全性和逻辑内聚性。
04 核心差异对比表
| 维度 |
Vue 2.x |
Vue 3.x |
核心差异点解析 |
| 响应式底层 |
Object.defineProperty |
Proxy |
Proxy 支持拦截对象/数组全操作,初始化由递归改为懒代理。 |
| Diff 算法 |
双端比较,全量遍历 |
PatchFlags + LIS + Block Tree |
Vue 3 跳过静态节点,Diff 性能仅与动态节点数量正相关。 |
| 代码组织 |
Options API (data/methods) |
Composition API (setup) |
逻辑聚合,彻底解决 Mixin 弊端,利于 Tree Shaking。 |
| TS 支持 |
弱(依赖 this,类型推断难) |
强(源码由 TS 重写) |
几乎不需要 this,泛型支持极佳。 |
| 生命周期 |
beforeDestroy, destroyed |
onBeforeUnmount, onUnmounted |
命名更语义化,需在 setup 中显式引入。 |
| 根节点 |
只能有 1 个 |
Fragment (支持多个) |
减少了无意义的包装 div,DOM结构更简洁。 |
| 组件通信 |
props, emit, $listeners, EventBus |
props, emit, provide/inject, expose |
移除了 $on/$off(EventBus),推荐使用外部库(如 mitt)或 Pinia。 |
| 全局 API |
Vue.prototype, Vue.use |
app.config.globalProperties, app.use |
应用实例化 (createApp),避免全局状态污染。 |
| 构建体积 |
较大(包含所有功能) |
极小(按需引入) |
未使用的功能(如 Transition)不会被打包。 |
| IE 兼容性 |
支持 IE9+ |
放弃 IE11 |
Proxy 无法被 Polyfill。 |
05 迁移注意:Vue 3 中移除的API
升级旧项目时,需要特别注意以下在 Vue 3 中被移除或改变的特性:
- Filters (过滤器):
{{ date \| format }} 语法已移除,请改用计算属性(computed)或方法调用。
- EventBus:实例方法
$on, $off, $once 已移除。跨组件通信推荐使用 provide/inject 或第三方事件库(如 mitt)。
$children:已移除。访问子组件应使用模板 ref。
.sync 修饰符:其功能已合并到 v-model 的带参数形式中,如 v-model:title。
结语
Vue 3 的演进,是算法优化(最长递增子序列)、语言特性应用(Proxy)与编译策略创新(PatchFlags、Block Tree)的全面胜利。它不再仅仅是一个易用的视图层库,而是一个在运行时性能与编译时优化上都极具竞争力的现代前端框架。
理解这些底层差异,能帮助我们在技术选型、性能优化和应对深度面试时更加从容。如果你希望与更多开发者交流此类前沿技术实践,欢迎来到云栈社区参与讨论。