请系统讲讲 Vue2 与 Vue3 的核心差异(响应式、API 设计、性能与编译器)。
一、 响应式系统:从"被动打补丁"到"原生降维打击"
这是两代框架最本质的鸿沟。Vue 2 是属性劫持 ,Vue 3 是对象代理。
1. 技术方案对比
-
Vue 2 (
Object.defineProperty) :- 原理 :在组件初始化时,必须深度递归遍历
data中的所有属性,将它们强行转为 getter/setter。 - 致命缺陷 :无法监听对象动态新增、删除 的属性;无法完美监听数组索引和长度 的变化。因此被迫发明了
this.$set()和this.$delete()这种打补丁的 API。
- 原理 :在组件初始化时,必须深度递归遍历
-
Vue 3 (
Proxy) :- 原理 :直接代理整个外部对象,拦截包含
get、set、deleteProperty在内的 13 种底层操作。 - 降维打击 :天生支持动态增删属性、支持数组任意操作。并且是懒代理(Lazy) ------只有当你真正读取深层嵌套对象时,才会动态将其包装成 Proxy,初始化性能暴增。
- 原理 :直接代理整个外部对象,拦截包含
2. 依赖收集模型差异
- Vue 2 内部每个属性都有对应的
Dep类,通过Watcher建立连接,闭包强引用较多。 - Vue 3 彻底解耦,使用全局统一的
WeakMap (targetMap)→Map→Set拓扑图谱管理依赖,弱引用对垃圾回收(GC)极度友好,杜绝了大部分内存泄漏。
二、 API 设计:从"巨型黑盒"到"积木解耦"
1. Options API(Vue 2)vs Composition API(Vue 3)
-
Vue 2 (Options API) :
- 代码被迫分散在
data、methods、computed、watch等固定的"选项型黑盒"中。 - 痛点 :当一个组件超过 500 行时,修改一个业务逻辑需要鼠标上下疯狂翻滚;且组件间逻辑复用靠
mixins,极易发生命名冲突 和数据来源不明(黑盒隐患) 。
- 代码被迫分散在
-
Vue 3 (Composition API /
<script setup>) :- 基于函数组合,将同一个业务功能的数据、方法、副作用完美聚集在一起。
- 爽点 :逻辑复用直接提取为自定义 Hook(如
useUser),数据来源清晰,完美解决 Options API 的割裂感。
2. 类型系统(TypeScript)
- Vue 2 的源码是基于 Flow 写的,对 TS 的支持属于"硬贴上去的壳子",大量的
this上下文让 TS 根本无法正确推导类型。 - Vue 3 整个源码完全用 TypeScript 重写,天生具备完美的类型推导与极致的 IDE 代码提示。
三、 性能与编译器:从"全量比对"到"精准靶向"
Vue 3 能够比 Vue 2 快数倍,核心功劳在于编译器(Compiler)变聪明了,它在编译时给运行时(Runtime)留下了大量的"作弊条"。
1. 静态提升 (Static Hoisting)
- Vue 2 :无论模板里的 HTML 是静态的(比如
<h1>固定标题</h1>)还是动态的,每次组件刷新,都会全部重新创建虚拟 DOM 节点(VNode),浪费大量内存。 - Vue 3 :编译器极其聪明,它会自动识别出那些永远不会变的静态节点,将其提升到渲染函数之外只创建一次。每次刷新时,直接复用同一个内存地址的 VNode。
2. 靶向更新:纯贴条机制 (PatchFlags)
- Vue 2 :数据变了之后,虚拟 DOM 会进行全量 Diff(哪怕页面有一万个节点,只要有一个变了,两棵树就要挨个节点对比一遍)。
- Vue 3 :在编译阶段,如果发现某个节点是动态的(比如
<span :class="cls">{{ text }}</span>),编译器就会在这个 VNode 后面打上一个数字标签(PatchFlag,比如代表动态文本和动态 class 的位掩码)。 - Diff 时的降维打击 :Vue 3 在 Diff 时,直接跳过所有静态节点,顺着动态节点链表(Block Tree)进行靶向更新,并且结合位运算,精准知道这个节点是只有文本变了还是只有 class 变了。
3. 缓存事件处理函数 (Event Handler Cache)
- Vue 2 中绑定的点击事件
@click="doSomething",每次渲染都会重新生成一个新的匿名函数。 - Vue 3 默认开启事件缓存,把事件函数缓存起来。这样只要函数没变,对应的组件节点就被视为静态的,直接跳过 Diff。
🧱 第一部分:认识 Proxy 的本质(基础权限拦截)
核心原理: Proxy 本质上是对一个目标对象建立的"内存拦截层"。我们实例化 Proxy 时需要传入两个参数:一个是"目标对象"(target),另一个是"处理器对象"(handler)。
JavaScript
javascript
// 1. 目标对象(Target / 明星):纯粹的数据载体,不自带任何防御逻辑
const rawUser = {
name: "张三",
role: "user",
_passwordHash: "xyz123456", // 约定的私有属性
};
// 2. 处理器对象(Handler / 经纪人):在这里定义拦截规则(陷阱方法)
const securityHandler = {
// 拦截 [[Get]] 操作:当外部尝试"读取"属性时触发
get(target, property, receiver) {
// 规则 1:如果是以下划线开头的属性,视为私有,拒绝访问
if (property.startsWith('_')) {
console.warn(`[警告]:无权访问私有属性 ${property}!`);
return undefined;
}
// 正常属性:记录日志并放行
console.log(`[日志]:有人读取了属性【${property}】`);
return Reflect.get(target, property, receiver); // 推荐使用 Reflect 底层转发
},
// 拦截 [[Set]] 操作:当外部尝试"写入/修改"属性时触发
set(target, property, value, receiver) {
// 规则 2:权限校验,防止越权修改角色
if (property === 'role' && value === 'admin') {
console.error(`[拒绝]:普通用户不能自己升级为 admin!`);
return false; // 严格模式下会抛出 TypeError,明确告知修改失败
}
// 正常修改:记录日志并放行
console.log(`[日志]:属性【${property}】被修改为了【${value}】`);
return Reflect.set(target, property, value, receiver); // 必须返回 true 代表成功
}
};
// 3. 实例化代理对象(完全接管对原对象的访问)
const protectedUser = new Proxy(rawUser, securityHandler);
🚨 基础拦截拦截测试
JavaScript
ini
// --- 场景 A:正常读取 ---
console.log(protectedUser.name);
// 输出: [日志]:有人读取了属性【name】 -> 张三
// --- 场景 B:试图窥探隐私(被拦截) ---
console.log(protectedUser._passwordHash);
// 输出: [警告]:无权访问私有属性 _passwordHash! -> undefined
// --- 场景 C:正常修改数据 ---
protectedUser.name = "李四";
// 输出: [日志]:属性【name】被修改为了【李四】
// --- 场景 D:试图进行非法越权操作(被拦截) ---
protectedUser.role = "admin";
// 输出: [拒绝]:普通用户不能自己升级为 admin!
🚀 第二部分:进阶应用(Vue 3 响应式系统模拟)
基于 Proxy 的拦截能力,我们可以无缝升级为 Vue 3 的依赖收集与更新机制 。你的理解非常准确:Proxy 是负责抓现场的"哨兵",而 effect 是告诉浏览器哪些 DOM 绑了数据的"登记处"。
1. 全局状态与依赖大仓库
JavaScript
javascript
// 临时存储当前正在执行的副作用函数(如当前正在渲染的组件)
let activeEffect = null;
// 全局依赖大仓库(结构:target -> key -> deps)
const targetMap = new WeakMap();
2. 双剑合璧:依赖收集 (Track) 与 派发更新 (Trigger)
JavaScript
ini
/**
* 依赖收集:在读取属性时,把当前的 activeEffect 存入对应的属性桶中
*/
function track(target, key) {
if (!activeEffect) return; // 如果不是在 effect 环境下读取的数据,直接放行
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);
}
dep.add(activeEffect); // 把当前副作用函数收集进桶里(Set 自动去重)
console.log(`[依赖收集] 成功收集依赖 -> 属性【${key}】依赖了当前 effect`);
}
/**
* 派发更新:在修改属性时,把属性桶里的所有 effect 拿出来重新执行
*/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
console.log(`\n[派发更新] 属性【${key}】发生改变,开始通知依赖项更新...`);
dep.forEach(effect => effect()); // 挨个执行登记过的副作用函数
}
}
3. 响应式入口与副作用包装(reactive / effect)
JavaScript
javascript
/**
* 将普通对象包装为基于 Proxy 的响应式对象
*/
function reactive(target) {
if (typeof target !== 'object' || target === null) return target;
return new Proxy(target, {
// 拦截读取 -> 触发依赖收集
get(target, key, receiver) {
track(target, key);
const res = Reflect.get(target, key, receiver);
// 【深层响应式优化】惰性代理:只有在读取到深层对象时,才实时将其转为 Proxy
if (typeof res === 'object' && res !== null) {
return reactive(res);
}
return res;
},
// 拦截修改 -> 触发派发更新
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 只有当新旧值发生真实改变时,才触发更新,避免无意义的重复渲染
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
});
}
/**
* 副作用包装器:让传入的函数具备响应式追踪能力
*/
function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn; // 1. 将自己挂载到全局变量上
return fn(); // 2. 执行函数,触发内部 reactive 数据的 get -> track
} finally {
activeEffect = null; // 3. 执行完毕后洗白,释放全局指针
}
};
effectFn(); // 默认立即执行一次,进行初次渲染和依赖收集
return effectFn;
}
🧪 第三部分:响应式联动测试
JavaScript
javascript
// ---- 测试数据 ----
const user = reactive({
name: "张三",
age: 18,
info: { city: "北京" } // 测试深层代理
});
// ---- 模拟 Vue 的组件渲染 (Effect 1) ----
effect(() => {
console.log(`【视图更新 🎬】界面渲染 -> 姓名: ${user.name}, 年龄: ${user.age}`);
});
// ---- 模拟另一个 Watch 监听器 (Effect 2) ----
effect(() => {
console.log(`【日志监控 📝】城市变成了: ${user.info.city}`);
});
console.log("\n--- 初始化收集完毕,开始修改数据 ---\n");
// 修改测试 1:修改普通属性(触发 Effect 1)
user.age = 19;
// 修改测试 2:修改深层嵌套属性(触发 Effect 2)
user.info.city = "上海";