文章

Proxy 响应式原理深度解析

Vue3 响应式基于 Proxy,能代理整个对象的 get、set、deleteProperty 等十多种操作,一举解决 Vue2 defineProperty 的三大死穴。 配合 Reflect 保住 this 与原型链,再用 WeakMap 三层结构追踪依赖,面试常考为什么 Proxy 能监听数组下标和动态增删属性。

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 响应式的三根柱子,缺任何一根房子都站不稳。

本文由作者按照 CC BY 4.0 进行授权