Vue 3.0 的响应式原理
Vue 3.0 的响应式系统相较于 Vue 2.x 发生了根本性的变化。核心在于它抛弃了 Vue 2 中的 Object.defineProperty,转而使用 ES6 的 Proxy 和 Reflect API。
以下是 Vue 3.0 响应式原理的详细解析,分为核心机制、工作流程、与 Vue 2 的对比以及代码实现模型四个部分。
1. 核心机制:Proxy 与 Reflect
Vue 3 使用 Proxy 对象创建一个代理,拦截对原始对象的所有操作(读、写、删除等)。
- Proxy (代理): 就像在目标对象外面包裹了一层壳。当你访问或修改数据时,必须先经过这层壳。这使得 Vue 能够捕获到任何对数据的变动。
- Reflect (反射): 配合 Proxy 使用。在 Proxy 的拦截器(Handler)中,使用
Reflect来执行原本要在对象上进行的操作(如Reflect.get()或Reflect.set())。它的主要作用是确保this指向正确,并返回操作结果。
2. 响应式工作流程
Vue 3 的响应式系统主要由三个动作组成:拦截 (Intercept)、依赖收集 (Track) 和 派发更新 (Trigger)。
A. 拦截 (Intercept)
当你把一个普通对象传给 reactive 函数时,Vue 会返回这个对象的 Proxy 实例。
- Get (读取): 当读取属性时,触发
get拦截器。 - Set (写入): 当修改属性时,触发
set拦截器。 - Delete (删除): 当删除属性时,触发
deleteProperty拦截器。
B. 依赖收集 (Track) - 在 get 中进行
当某个副作用函数(Effect,例如组件的渲染函数、computed、watch)读取响应式数据时:
- 触发 Proxy 的
get拦截器。 - 调用
track函数。 - Vue 会将当前正在运行的副作用函数(
activeEffect)记录下来,存入一个全局的 WeakMap 数据结构中。
存储结构:targetMap (WeakMap) -> key: target对象, value: depsMap (Map)depsMap (Map) -> key: 属性名, value: dep (Set)dep (Set) -> value: effect函数
简单理解: Vue 记了个小本本:哪个对象的哪个属性,被哪个函数用到了。
C. 派发更新 (Trigger) - 在 set 中进行
当响应式数据被修改时:
- 触发 Proxy 的
set拦截器。 - 调用
trigger函数。 - Vue 根据目标对象和属性名,去
targetMap中找到对应的dep集合。 - 遍历集合,执行所有相关的副作用函数(比如重新渲染组件)。
3. 为什么比 Vue 2 好? (Vue 3 vs Vue 2)
Vue 2 使用 Object.defineProperty 将属性转化为 getter/setter。Vue 3 使用 Proxy 有以下显著优势:
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 新增/删除属性 | 无法监听 (需要 Vue.set / Vue.delete) |
原生支持 (直接 obj.newProp = x 即可) |
| 数组监听 | 受限 (不能监听索引和 length 变化,重写了数组方法) | 原生支持 (直接修改索引或 length 均可) |
| 嵌套对象性能 | 递归遍历 (初始化时一次性递归所有层级,大对象性能差) | 懒代理 (Lazy) (只有访问到嵌套属性时才通过 Proxy 代理下一层) |
| 数据结构支持 | 仅支持 Object | 支持 Map, Set, WeakMap, WeakSet |
4. 简易代码实现模型
为了帮助理解,这里写一个极简版的 Vue 3 reactive 实现:
// 1. 存储副作用的容器
let activeEffect = null;
// 2. 依赖收集器 (数据结构: WeakMap -> Map -> Set)
const targetMap = new WeakMap();
// 3. track: 收集依赖
function track(target, key) {
if (!activeEffect) return; // 如果没有正在执行的副作用,直接返回
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); // 将当前副作用函数加入依赖集合
}
// 4. trigger: 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect()); // 执行所有依赖该属性的函数
}
}
// 5. reactive: 创建响应式对象
function reactive(target) {
// 如果不是对象,直接返回
if (typeof target !== 'object' || target === null) {
return target;
}
const handler = {
get(target, key, receiver) {
// 收集依赖
track(target, key);
const result = Reflect.get(target, key, receiver);
// 递归代理(懒代理):只有当访问到深层属性时,才对该属性进行响应式处理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
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;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key); // 删除属性也要触发更新
return result;
}
};
return new Proxy(target, handler);
}
// --- 测试 ---
// 模拟一个副作用函数(比如组件渲染)
function effect(fn) {
activeEffect = fn;
fn(); // 立即执行一次,触发 get 从而收集依赖
activeEffect = null;
}
const state = reactive({ count: 0, user: { name: 'Vue' } });
effect(() => {
console.log('UI 更新了,count:', state.count);
});
state.count++; // 控制台输出: UI 更新了,count: 1
5. 关于 ref 的原理
你可能会问,Proxy 只能代理对象,那基本数据类型(如 string, number)怎么办?
这就是 ref 的作用。
ref内部并没有使用 Proxy 来代理基本类型(因为 Proxy 无法代理基本类型)。ref创建了一个对象(RefImpl类的实例),该对象有一个.value属性。- 它利用了 getter 和 setter (类似于 Vue 2 的方式) 来拦截对
.value的访问和修改,从而实现依赖收集和更新触发。 - 如果
ref接收的是一个对象,它内部会自动调用reactive将其转化为 Proxy。
总结
Vue 3 响应式原理的核心是 Proxy。
- Proxy 拦截对象操作。
- Getter 中进行 Track (依赖收集),建立数据与副作用函数的映射关系。
- Setter 中进行 Trigger (派发更新),执行副作用函数。
- 相比 Vue 2,它更强大(支持数组、新增属性)、更高效(懒代理)。