文章

Proxy 响应式原理

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/splicelist.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 没有任何关系。正确做法是用 toRefsconst { 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 少写一半代码,多拦截一倍操作。

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