前言
做前端架构的人要不要懂 Vue 内部?我的回答是 要——不是去读 @vue/reactivity 源码,而是建立”为什么 Vue 3 比 Vue 2 更稳、更快”的工程判断。原因有三:
- 响应式调优需要它。为什么
ref和reactive不能直接互换?为什么 computed 缓存何时失效?不懂原理就只能”用模板能跑就行”。 - 大项目靠 Vue 守门。ERP 财务系统 200+ 表格列 + 80+ 表单字段,用 Vue 3 的响应式 + Composition API + 类型体操,重构成 Vue 2 重写不了的项目结构。
- 跨框架迁移需要它。简历里出现 Vue 2 / Vue 3 同时存在,理解 v2→v3 的 Proxy 重构动机,才知道”哪些 v2 写法在 v3 是反模式”。
这一篇是『前端架构修仙路』的第 9 篇。我跳过 reactivity 包源码(不展开 createReactiveObject 的每个分支),用架构图 + 时序图 + 真实案例,把 Vue 3 的 Proxy 响应式 / 依赖追踪 / VNode Diff / Composition API 讲透。下一篇深度聊 Angular 的企业级架构与 DI。
一、Vue 的核心心智:响应式数据驱动
<!-- Vue 心智模型:模板 + 数据 -->
<template>
<div>
<p>{{ count }}</p>
<button @click="count++">+</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0); // 声明响应式数据
</script>
核心三要素:
- 响应式数据:用
ref/reactive包装,框架自动追踪依赖 - 模板渲染:模板编译成 render function,数据变化自动更新 DOM
- 事件回调:模板里
@click直接写 JS,不需要.addEventListener
二、Vue 2 → Vue 3 响应式重构:为什么换 Proxy
2.1 Vue 2 的 Object.defineProperty 痛点
Vue 2 用 Object.defineProperty 实现响应式:
// Vue 2 响应式原理(简化)
function defineReactive(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
// 追踪:把当前 watcher 加入依赖
dep.depend();
return value;
},
set(newVal) {
// 触发:通知所有依赖更新
dep.notify();
value = newVal;
},
});
}
三大痛点:
- 无法检测新增属性:
vm.newProp = 1不触发更新(必须Vue.set) - 无法检测数组下标变化:
arr[0] = 1/arr.length = 0不触发 - 无法检测 Map / Set:要重写一堆方法(push / pop / shift 等)
2.2 Vue 3 的 Proxy 重构
Vue 3 用 Proxy(ES2015 新特性)替代:
// Vue 3 响应式原理(简化)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key); // 追踪:记录哪个 effect 依赖了这个 key
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key); // 触发:通知所有依赖更新
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
},
});
}
三大改进:
- ✅ 新增 / 删除属性自动响应(
delete obj.prop也触发) - ✅ 数组下标 / length 变化自动响应
- ✅ Map / Set / WeakMap / WeakSet 完整支持
性能:Proxy 比 defineProperty 更快——前者是引擎原生实现,后者要遍历每个 key。
三、依赖追踪:track / trigger / effect 三件套
Vue 3 响应式的核心是 三个全局数据结构:
// 全局 WeakMap:target → Map<key, Set<effect>>
const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 把当前正在运行的 effect 加入依赖
if (activeEffect) {
dep.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => effect.run()); // 触发更新
}
}
3.1 reactive vs ref:什么时候用哪个
import { reactive, ref } from 'vue';
// reactive:对象 / 数组
const state = reactive({ count: 0, user: { name: 'Alice' } });
state.count++; // 自动响应
state.user.name = 'Bob'; // 嵌套也响应
// ref:基本类型 / 需要整体替换的值
const count = ref(0);
count.value++; // 注意 .value
架构师规则:
ref包基本类型 + DOM 引用 + 需要整体替换的对象reactive包会多个属性独立修改的对象- 模板里 ref 自动解包(不需要
.value),reactive 不会
3.2 computed 的缓存机制
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
// doubled.value 第一次访问 → 执行计算
// doubled.value 第二次访问 → 命中缓存(count 没变)
// count.value = 1 → 缓存失效,下次访问重新计算
生产案例:某 FinSpread 表格 100 列 × 500 行,computed 缓存避免重复计算,渲染性能从 800ms 降到 50ms。
3.3 watch vs watchEffect:副作用的选择
// watch:显式依赖,旧值 / 新值
watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`);
});
// watchEffect:自动追踪所有响应式依赖,初始化时立即跑
watchEffect(() => {
console.log(`count is ${count.value}`); // 依赖 count
});
// 停止监听(防止泄漏)
const stop = watch(...);
stop(); // 主动停止
架构师规则:能 watchEffect 就 watchEffect——自动追踪更安全(不会漏掉依赖);需要旧值时才用 watch。
四、VNode Diff 算法:Vue 3 的编译期优化
4.1 双端比较算法(Vue 2)
Vue 2 用 双端 diff:
- 头尾 4 个指针同步比较
- 时间复杂度 O(n)
- 但静态节点每次也要 diff
4.2 Vue 3 的 Block Tree(编译期提速)
Vue 3 编译时给模板加 patchFlag(编译提示),只 diff 动态部分:
// Vue 3 编译产物(简化)
function render(_ctx) {
return openBlock(), createElementBlock("div", null, [
createElementVNode("h1", null, "Hello"), // patchFlag = STATIC(0),跳过 diff
createElementVNode("p", null, _ctx.count, 1 /* TEXT */), // patchFlag = TEXT,只 diff 文本
createBlock("div", null, [
forEach(_ctx.list, (item) => {
return createElementVNode("li", { key: item.id }, item.name, 1 /* TEXT */);
})
])
]);
}
核心收益:静态节点直接跳过 diff,只对带 patchFlag 的动态节点做比较。生产数据:模板 70% 静态节点的项目,渲染速度提升 30-50%。
4.3 Block Tree:动态节点边界
Vue 3 在模板编译时自动识别动态节点的边界:
<template>
<div> <!-- 整个 div 是 block,包含两个动态子节点 -->
<h1>{{ staticTitle }}</h1> <!-- 动态文本 -->
<ul>
<li v-for="item in list">{{ item.name }}</li> <!-- 列表是 fragment block -->
</ul>
</div>
</template>
编译产物会自动识别 v-if / v-for 创建的子节点,整个 block 在 diff 时扁平化——避免跨 block 的无效比较。
五、Composition API:函数式思维统一
5.1 Options API vs Composition API
// Options API:Vue 2 经典写法,声明式
export default {
data() {
return { count: 0, user: null };
},
computed: {
doubled() { return this.count * 2; }
},
methods: {
increment() { this.count++; }
},
mounted() { this.fetchUser(); }
};
// Composition API:Vue 3 新写法,函数式
import { ref, computed, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
const increment = () => count.value++;
onMounted(() => fetchUser());
return { count, doubled, increment };
}
};
5.2 Composition API 的真正价值
| 维度 | Options API | Composition API |
|---|---|---|
| 状态逻辑复用 | mixin(隐式合并,易冲突) | composable 函数(显式调用) |
| 大组件拆分 | 拆组件(重渲染开销大) | 拆 composable(无渲染开销) |
| TS 类型推导 | 弱(this 难推导) | 强(ref / computed 类型清晰) |
| 跨组件代码共享 | 难(必须用 mixin 或 HOC) | 易(直接 import composable) |
生产案例:某 ERP 财务系统 200+ 表格列,每个列的”过滤 / 排序 / 编辑”逻辑用 composable 抽出,3 个核心 composable(useTable / useForm / usePermission)支撑了 800+ 页面。Options API 时代这种复用需要 mixin 或 HOC,复杂度高 10 倍。
5.3 三个核心 composable 模式
// 1. useAsyncData:异步数据 + loading / error
function useAsyncData(fetcher) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const run = async () => {
loading.value = true;
try {
data.value = await fetcher();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
};
onMounted(run);
return { data, loading, error, run };
}
// 2. useForm:表单状态 + 校验
function useForm(initial, rules) {
const values = reactive({ ...initial });
const errors = reactive({});
const validate = (field) => {
const rule = rules[field];
if (!rule) return true;
if (!rule.pattern.test(values[field])) {
errors[field] = rule.message;
return false;
}
delete errors[field];
return true;
};
const submit = async (handler) => {
const allValid = Object.keys(rules).every(validate);
if (!allValid) return false;
await handler(values);
return true;
};
return { values, errors, validate, submit };
}
// 3. usePermission:权限检查
function usePermission() {
const permissions = inject('permissions');
const can = (action) => permissions.value.includes(action);
return { can };
}
六、Vapor Mode(Vue 3.5):无 VDOM 的未来
6.1 核心思路
Vapor mode(2024-2025)让 Vue 编译器绕过 VNode Diff,编译时直接生成 DOM 操作代码:
// Vapor 编译产物(简化,概念演示)
function render(_ctx) {
// 不创建 VNode,直接操作 DOM
const div = document.createElement('div');
const p = document.createElement('p');
const update = () => p.textContent = _ctx.count;
_ctx.count.watch(update); // 状态变化直接更新 p.textContent
div.appendChild(p);
return div;
}
6.2 收益
- Bundle size 减少 50%(不打包 Vue runtime)
- 首屏渲染快 1.5-3 倍(无 VNode 创建 / diff 开销)
- 更接近 Solid.js 的细粒度响应式
生产案例:某中后台页面 Vapor mode 重构后,首屏 JS 从 280KB 降到 130KB(-54%),LCP 从 1.8s 降到 1.1s(-39%)。
七、跨框架组件复用:Web Components + Vue + React
简历里出现 “8 个跨 Vue/React 通用组件”,核心抽象层是 Web Components:
// 用 Web Components 写一次,Vue / React / Angular 都可用
class SmartTable extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<table>
<slot></slot>
</table>
`;
}
}
customElements.define('smart-table', SmartTable);
<!-- Vue 里用 -->
<smart-table :columns="cols"><tr><td>...</td></tr></smart-table>
// React 里用(用 @lit/react 包装)
import { createComponent } from '@lit/react';
const SmartTable = createComponent({ react: SmartTableReact, ... });
架构师规则:跨框架组件库的核心是 Web Components 抽象,业务代码再各自用框架 wrapper。避免在 Vue 里硬塞 React 组件库。
八、踩坑提醒(资深架构师请重点看)
- 不要在 reactive 对象上整体替换。
state = newState会丢失所有响应式追踪。必须用Object.assign(state, newState)或保持引用稳定。 - 不要滥用
watch。watch 的隐式依赖(deep / flush)容易踩坑。能 computed 就 computed,只有需要”执行副作用”才 watch。 - 不要在 SSR 里用
window/document。Nuxt 3 / Vite SSR 默认在 Node 环境执行,直接访问 window 报错。用import.meta.client守护。 - 不要混淆 ref / reactive 的模板语法。
ref自动解包,reactive 不会——模板里count(ref) 等价于count.value,但state.count(reactive) 等价于state.count。切换类型时检查一遍模板。 - 不要在 v-for 里用 index 当 key。
<li v-for="(item, i) in list" :key="i">在列表 reorder / 增删时会复用错误 DOM 节点,状态错乱。用稳定的唯一 ID。
总结
这一篇用 7 个关键事实把 Vue 3 内部机制串起来:
- Proxy 替代 defineProperty:新增 / 删除属性、数组下标、Map / Set 全部响应式。
- 依赖追踪三件套:track / trigger / effect 用 WeakMap + Set 实现精确依赖追踪。
- computed 缓存:依赖不变 → 命中缓存;依赖变 → 失效重算。
- Block Tree + patchFlag:编译期识别动态节点,只 diff 动态部分,渲染速度提升 30-50%。
- Composition API:函数式 composable 替代 mixin / HOC,大项目复用代码量减少 70%。
- Vapor mode(Vue 3.5):无 VDOM,首屏 JS 减半,LCP 降 39%。
- Web Components:跨 Vue / React / Angular 通用组件的核心抽象层。
下一篇:Angular 深度——企业级 DI 体系 + RxJS 异步原语 + 强约定架构,从信锐时期 Ionic + Angular 实战讲起。
5 道重点面试问题方向
Q1(答案):Vue 2 用
Object.defineProperty实现响应式,Vue 3 为什么改用Proxy?解决了哪些问题?A:Vue 2 痛点:① 无法检测新增属性 ——
vm.newProp = 1不触发更新,必须Vue.set;② 无法检测数组下标变化 ——arr[0] = 1/arr.length = 0不触发;③ 无法检测 Map / Set —— 要重写 push/pop/shift 等方法。Vue 3 Proxy 解决:Proxy 是对象级别的拦截(不是属性级别),所以新增 / 删除 / 数组下标 / Map / Set 全部自动响应;同时 Proxy 是引擎原生实现,比 Object.defineProperty 性能更好。面试要点:能说”Proxy 拦截的是对象整体,defineProperty 拦截的是单个属性”——这是核心区别。Q2(思考):Vue 3 的
ref和reactive什么时候该用哪个?模板里有什么差异?Q3(思考):Vue 3 的 Block Tree + patchFlag 编译期优化是怎么工作的?为什么能让渲染速度提升 30-50%?
Q4(思考):Composition API 和 Options API 各自的优劣是什么?在 200+ 表格列的 ERP 项目里你选哪个?为什么?
Q5(思考):你在 ERP 财务系统里是怎么从 Vue 2 迁移到 Vue 3 的?踩过哪些坑?最大的教训是什么?
💡 每道题后面都有 AI 助手按钮,可以一键拿到详细答案。
参考资料
- Vue 3 Reactivity (Vue.js Official) — 响应式官方深度指南
- Proxy vs Object.defineProperty (Vue 3 RFC) — Vue 3 响应式重构 RFC
- Composition API RFC — Composition API 设计 RFC
- Vapor Mode (Vue 3.5 Roadmap) — 无 VDOM 模式讨论
- Vue 3 Migration Guide — 官方 v2→v3 迁移指南
- Mastering Vue 3 Reactivity (Lachlan Miller) — 响应式源码解读