Proxy 响应式原理深度解析
Vue3 响应式基于 Proxy,能代理整个对象的 get、set、deleteProperty 等十多种操作,一举解决 Vue2 defineProperty 的三大死穴。 配合 Reflect 保住 this 与原型链,再用 WeakMap 三层结构追踪依赖,面试常考为什么 Proxy 能监听数组下标和动态增删属性。
一句话概括
Vue 3 响应式基于 Proxy——它能代理整个对象的 get/set/deleteProperty 等 13 种操作,一举解决 Vue 2 Object.defineProperty 三大死穴:数组下标监听、属性动态增删、深层递归性能。配上 Reflect 保住 this 和原型链,再通过 WeakMap → Map → Set 三层结构精准追踪依赖。
核心知识点
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)}`);
return Reflect.get(target, key, receiver); // Reflect 保证 this 正确
},
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`);
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.name; // "删除 name" ← Vue 2 里这行不会触发更新
一句话区分: defineProperty 是给每个属性配一把锁(遍历成本高),Proxy 是给整个房子围一圈墙(一把梭全拦截)。
2. 手写 reactive 核心——30 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const targetMap = new WeakMap();
let activeEffect = null;
const reactive = (target) => 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;
}
});
三个设计点: (1)Reflect 不是可选的——它传递 receiver 保证原型链上 this 不跑偏;(2)懒递归——只代理访问到的嵌套对象,不是初始化时全量递归;(3)old !== value 比较避免无意义更新(NaN 需要额外处理)。
3. Proxy vs defineProperty ——一张表说清
| Proxy(Vue 3) | defineProperty(Vue 2) | |
|---|---|---|
| 粒度 | 对象级(一个代理全覆盖) | 属性级(每个属性逐个设) |
| 新增属性 | ✅ 自动拦截 | ❌ 需要 $set |
| 删除属性 | ✅ deleteProperty | ❌ 无法劫持 |
| 数组下标 | ✅ 直接拦截 | ❌ 抛弃,改重写 7 个数组方法 |
| 初始化性能 | 惰性递归(用到才代理) | 全量递归(初始化就遍历) |
| IE11 | ❌ 无 polyfill | ✅ |
4. Reflect 为什么不可或缺
1
2
3
4
5
6
7
8
// ❌ 错误写法:this 丢失
get(target, key) { return target[key]; }
// ✅ 正确:Reflect.get 的第三个参数 receiver 保证 getter 中的 this 指向 proxy
const parent = {
get name() { return this._name; } // this 必须指向 proxy,否则收集不到依赖
};
const child = Object.create(parent);
没有 Reflect 的后果: 原型链上的 getter 里的 this 指向原始对象,track 收不到依赖,数据变了视图不更新。
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 } 的 getter/setter
state.count++; // 直接访问
count.value++; // 模板中自动解包,JS 里必须 .value
// ref 传对象时内部调用 reactive
const obj = ref({ a: 1 });
obj.value.a = 2; // ✅ 响应式(内部是 Proxy)
其实你每天都在用
v-model双向绑定:<input v-model="form.name">→ 每敲一个字符触发set拦截 → effect 重跑 → DOM 更新- 数组
push/splice:list.push(item)→ Proxy 拦截 length 变化 → 列表自动重渲染(Vue 2 需要重写数组方法) - 动态加属性:
state.newField = 'value'→ 自动响应式(Vue 2 必须this.$set) - computed 缓存: getter 中读取的每个响应式变量都被 Proxy get 拦截 → track → 依赖变化只设 dirty
- devtools 调试: Vue 3 devtools 中数据标为
Proxy,展开就能看到整棵依赖树
常见误解(FAQ)
❌ 误区:「reactive 对象解构后还是响应式的」 绝对不会。
const { count } = reactive({ count: 0 })→ 解构出来的是普通0,跟 Proxy 断了联系。正确做法:toRefs(state)。❌ 误区:「Proxy 不支持 IE,所以 Vue 3 不兼容旧浏览器」 正是。Vue 2 选择
defineProperty因为要兼容 IE9+。Proxy 没有可靠的 polyfill(Google 的 proxy-polyfill 只覆盖 4 种拦截),Vue 3 直接放弃 IE。❌ 误区:「
JSON.parse(JSON.stringify(...))拷贝响应式对象没问题」 会丢失 Proxy 包装。深拷贝后的对象是”死了的”——再也不响应。需要取原始值用toRaw()。❌ 误区:「Reflect 只是 Proxy 的语法糖」 Reflect 的
receiver参数解决了原型链上 getter 的 this 指向问题。没有 receiver,继承来的属性访问不会触发子代理的 get 拦截——这是没 Reflect 就无法修复的 bug。
一句话总结
Proxy 接管对象的一切访问,Reflect 保证 this 不迷路,WeakMap 兜底内存不泄漏——Vue 3 响应式的三根柱子,缺任何一根房子都站不稳。