摘要 :Vue 3 的响应式系统用 ES6
Proxy+Reflect替代了 Vue 2 的Object.defineProperty,解决了无法检测属性增删、数组索引修改等痛点。核心由四个函数协作完成------reactive创建响应式代理、effect注册副作用函数、track在读取时收集依赖、trigger在修改时触发更新。这四者构成了一个"数据变化 → 自动更新视图"的完整闭环。
一、Vue 2 vs Vue 3:为什么换掉 Object.defineProperty?
Object.defineProperty 的局限(Vue 2)
js
// Vue 2 只能拦截「已有属性」的读写
Object.defineProperty(obj, 'a', {
get() { /* ... */ },
set(val) { /* ... */ }
});
obj.a = 1; // ✅ 能检测到
obj.b = 2; // ❌ 新增属性,检测不到!
delete obj.a; // ❌ 删除属性,检测不到!
arr[0] = 'x'; // ❌ 数组索引修改,检测不到!
arr.push(1); // ❌ 数组方法变异,需要 hack 重写
Proxy 的优势(Vue 3)
js
// Proxy 拦截的是整个对象,不关心具体属性
const proxy = new Proxy(target, {
get(target, key) { /* 拦截所有读取 */ },
set(target, key, val) { /* 拦截所有写入 */ },
deleteProperty(target, key) { /* 拦截删除 */ },
has(target, key) { /* 拦截 in 操作 */ }
});
proxy.newProp = 1; // ✅ 新增属性能检测到
delete proxy.a; // ✅ 删除属性能检测到
proxy.arr[0] = 'x'; // ✅ 数组索引修改能检测到
对比总结
| 维度 | Vue 2: Object.defineProperty | Vue 3: Proxy |
|---|---|---|
| 拦截粒度 | 逐个属性定义 getter/setter | 整个对象统一代理 |
| 属性新增/删除 | 需要手动 $set / $delete |
自动响应 |
| 数组 | 需要重写 7 个变异方法 | 原生支持 |
| Map / Set / WeakMap | 不支持 | 支持 |
| 性能 | 初始化时递归遍历所有属性 | 懒代理,访问时才处理 |
二、核心架构
javascript
┌──────────┐ ┌─────────────┐ ┌─────────┐
│ compiler │ │ reactivity │ │ runtime │
│ 编译器 │ │ 响应式核心 │ │ 运行时 │
└──────────┘ └──────┬──────┘ └─────────┘
│
┌─────────┴─────────┐
│ │
Object.defineProperty Proxy + Reflect
(Vue 2) (Vue 3)
reactivity 模块是响应式的核心,它不依赖 compiler 和 runtime,可以独立使用。
三、四大核心函数
1. reactive --- 创建响应式对象
用 Proxy 包装原始对象,返回一个代理。对代理的任何操作都会被拦截。
js
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 读取时:收集依赖 ------ "谁在读这个属性?"
track(target, key);
// 如果值是对象,递归包装(懒代理)
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);
if (result) {
trigger(target, key);
}
return result;
}
});
}
为什么用 Reflect?
Reflect.get/set和Proxy的 handler 参数一一对应,保证this指向正确- 直接操作原对象会绕过 Proxy,而
Reflect会走代理链 - 返回布尔值,方便判断操作是否成功
2. effect --- 注册副作用函数
副作用函数就是"当依赖的数据变化时,需要重新执行的函数"。它是 computed 和 watch 的基础。
js
// 全局变量:当前正在执行的 effect
let activeEffect = null;
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn; // 标记当前 effect
fn(); // 执行函数 → 触发 getter → 收集依赖
activeEffect = null; // 执行完清除标记
};
effectFn(); // 立即执行一次
return effectFn;
}
使用示例:
js
const state = reactive({ count: 0 });
effect(() => {
console.log('count 变了:', state.count);
// 这里读取了 state.count → 触发 getter → track 收集依赖
// 此时 activeEffect 就是当前的 effect 函数
});
state.count = 1; // 触发 setter → trigger → 重新执行 effect → 输出 "count 变了: 1"
3. track --- 依赖收集(在 getter 中调用)
记录"哪个 effect 依赖了哪个对象的哪个属性"。
js
// 数据结构:target → key → Set<effect>
// 即:某个对象的某个属性 → 被哪些 effect 使用
const targetMap = new WeakMap();
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); // 把当前 effect 加入依赖集合
}
数据结构示意:
javascript
targetMap (WeakMap)
└── target (原始对象)
└── depsMap (Map)
├── 'count' → Set [effectA, effectB]
└── 'name' → Set [effectA]
4. trigger --- 触发更新(在 setter 中调用)
找到依赖了这个属性的所有 effect,重新执行它们。
js
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
// 复制一份再遍历,避免在执行过程中修改 Set 导致问题
[...dep].forEach(effect => {
effect();
});
}
四、完整流程串联
用一个具体例子走一遍完整流程:
js
// 第一步:创建响应式数据
const state = reactive({ count: 0, name: 'vue' });
// 第二步:注册副作用(自动收集依赖)
effect(() => {
console.log(`count=${state.count}, name=${state.name}`);
});
/* 执行过程:
1. activeEffect = 当前 effectFn
2. 执行 fn()
3. 读 state.count → get 拦截 → track(state, 'count') → 记录依赖
4. 读 state.name → get 拦截 → track(state, 'name') → 记录依赖
5. activeEffect = null
输出: count=0, name=vue
*/
// 第三步:修改数据(触发更新)
state.count = 1;
/* 执行过程:
1. set 拦截 → Reflect.set → 值变了
2. trigger(state, 'count')
3. 找到依赖 count 的所有 effect → 重新执行
输出: count=1, name=vue
*/
流程图
scss
reactive(data) effect(fn)
│ │
▼ ▼
┌─────────┐ ┌──────────┐
│ Proxy │ │ 标记为 │
│ 代理对象 │ │ active │
└────┬────┘ └─────┬────┘
│ │
│ 读取属性 (get) │
◄──────────────────────┘
│
▼
track(target, key)
│
▼
targetMap 存入依赖关系
{ target: { key: [effect] } }
│ 修改属性 (set)
◄──────────────────────
│
▼
trigger(target, key)
│
▼
找到对应 effect → 重新执行 fn
(回到 effect 开始新一轮循环)
五、从源码到实际 API
上面的是简化版原理,Vue 3 实际暴露的 API:
| 原理函数 | 对应的 Vue API | 用途 |
|---|---|---|
reactive() |
reactive() |
创建深层响应式对象 |
reactive() 浅层版 |
shallowReactive() |
只代理第一层 |
reactive() + 只读 |
readonly() |
响应式但不可修改 |
effect() |
watchEffect() |
自动追踪依赖并执行回调 |
effect() + 显式指定 |
watch(() => src.value, cb) |
监听特定来源 |
effect() + 缓存 |
computed(() => expr) |
计算属性(带缓存) |
computed 的本质
computed 就是一个带缓存的 effect:
js
function computed(getter) {
let value;
let dirty = true; // 脏标记:是否需要重新计算
const effectFn = effect(() => {
value = getter();
dirty = false;
});
return {
get value() {
if (dirty) {
effectFn(); // 重新计算
}
return value; // 否则直接返回缓存
}
};
}
六、快速记忆口诀
- reactive:Proxy 包一层,读写全拦截
- effect:包住你的函数,执行时自动"报名"
- track:getter 里喊一声"谁在看我",记下来
- trigger:setter 里喊一声"我变了",通知所有人重来
- 一句话概括:读的时候收债(collect),写的时候还钱(notify)