Proxy 响应式原理
一句话概括
Vue 3 响应式基于 ES6 Proxy——它能拦截整个对象(而非单个属性)的 get/set/deleteProperty 等 13 种操作,一举解决 Vue 2 Object.defineProperty 的三大痛点:数组下标监听、属性动态增删、深层递归性能。
核心知识点
1. Proxy 基础——代理整个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const target = { name: 'Alice', age: 18 };
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`); // track(依赖收集)
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`); // trigger(触发更新)
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`); // Vue 2 做不到!
return Reflect.deleteProperty(target, key);
}
});
proxy.age = 30; // "设置 age = 30"
delete proxy.age; // "删除 age"
2. 手写简易 reactive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap();
let activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let deps = depsMap.get(key);
if (!deps) depsMap.set(key, (deps = new Set()));
deps.add(activeEffect);
}
function trigger(target, key) {
const deps = targetMap.get(target)?.get(key);
deps?.forEach(effect => effect());
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
// 懒代理:访问到的嵌套对象才递归包装
return (res && typeof res === 'object') ? reactive(res) : res;
},
set(target, key, value, receiver) {
const old = target[key];
const ok = Reflect.set(target, key, value, receiver);
if (old !== value) trigger(target, key);
return ok;
}
});
}
3. Proxy vs Object.defineProperty
| Proxy(Vue 3) | defineProperty(Vue 2) | |
|---|---|---|
| 拦截粒度 | 对象级别(一个代理全覆盖) | 属性级别(每个属性逐个 set) |
| 新增属性 | ✅ 自动拦截 | ❌ 需 Vue.set |
| 数组下标 | ✅ 自动拦截 | ❌ 需重写数组方法 |
| 删除属性 | ✅ deleteProperty | ❌ 无法劫持 |
| 性能 | 惰性递归(用到才代理) | 初始化时全部递归 |
核心差异一句话:defineProperty 是”给每扇门配一把锁”,Proxy 是”给整个房子围一圈墙”。
4. Reflect 不可或缺
1
2
3
4
5
// ❌ 错误:target[key] 不保持 this 绑定
get(target, key) { return target[key]; }
// ✅ 正确:Reflect 传递 receiver,保持原型链和 this
get(target, key, receiver) { return Reflect.get(target, key, receiver); }
5. reactive vs ref
1
2
3
4
5
6
7
8
9
10
11
import { reactive, ref } from 'vue';
const state = reactive({ count: 0 }); // 对象用 Proxy
const count = ref(0); // 原始值包装 { value: 0 }
state.count++; // 直接访问
count.value++; // 需 .value(模板中自动解包)
// ref 传对象时内部调用 reactive
const obj = ref({ a: 1 });
obj.value.a = 2; // ✅ 自动响应(内部也用了 Proxy)
其实你每天都在用
- v-model 双向绑定:
<input v-model="form.name">,输入每个字符都触发set拦截 → 视图更新 - 数组 push/splice:
list.push(item)触发 length 变化,列表自动重新渲染——Vue 2 做不到需要$set - 动态添加表单字段:
form.newField = 'value'自动响应,Vue 2 必须this.$set(form, 'newField', ...) - computed 缓存失效:依赖变化时只设 dirty 标记,不立即计算——背后是 Proxy 的 get 精确追踪
- devtools 响应式数据查看:Vue 3 devtools 中数据被标为
Proxy,就是这套机制的直接体现
常见误解
❌ 误区:「Proxy 完全替代了 defineProperty」 是的,Vue 3 不再使用 defineProperty 做响应式。但
ref处理原始值时用的是一个{ value: ... }对象上的 getter/setter,本质上又回到了”属性级拦截”的思路——只不过因为只有一个.value属性,没有 defineProperty 的规模化弊端。❌ 误区:「reactive 对象解构后仍然保持响应式」 不会!
const { count } = reactive({ count: 0 })——解构出来的是普通值,和 Proxy 没有任何关系。正确做法是用toRefs:const { count } = toRefs(state)。❌ 误区:「Proxy 不存在兼容性问题」 IE11 不支持且没有可靠的 polyfill。所以如果你的产品仍要支持 IE,Vue 3 不合适——这是 Vue 2 放弃 Proxy 选择 defineProperty 的根本原因。
❌ 误区:「
JSON.parse(JSON.stringify(...))拷贝响应式对象没问题」 会丢失 Proxy 包装!深拷贝后的对象不再是响应式的。如果需要拷贝响应式对象,用toRaw()取原始值再操作,或者用 lodash 的 cloneDeep 后重新reactive()包装。
一句话总结
Proxy 是 Vue 3 响应式的基石:对象级别的劫持 + 惰性递归 + Reflect 精确控制——比 defineProperty 少写一半代码,多拦截一倍操作。