纠正一个概念: Vue 2 使用的 Object.defineProperty 其实也是通过 get()set() 来拦截的。所以,区别不在于“有没有 get/set”,而在于 拦截的层级不同

我们可以用一句话概括核心区别:

  • Vue 2 (Object.defineProperty):“属性级” 拦截。它必须针对对象的每一个具体的 Key 进行单独的定义。
  • Vue 3 (Proxy):“对象级” 拦截。它直接代理了整个对象,不管里面有多少个 Key,或者未来会增加什么 Key,它都能管得着。

下面我们用更形象的例子和代码来拆解一下:


1. Vue 2:尽职的“门口保安” (Object.defineProperty)

Object.defineProperty 就像是你给大楼里的每一个房间(属性)都派了一个保安。

为什么监听不到对象新增属性?

当你初始化 Vue 2 时,它会遍历 data 里的所有属性(比如 name, age),给这几个已知的房间派了保安。

// Vue 2 初始化伪代码
let obj = { name: 'Gemini' };

// 只要你访问 obj.name,就会经过这个拦截
Object.defineProperty(obj, 'name', {
    get() { return val; },
    set(newVal) { updateView(); }
});

问题来了:
如果你突然在对象上加了一个新属性 obj.sex = 'male'(盖了一个新房间),因为初始化的时候这个房间不存在,所以没有派保安去守着
当你修改 obj.sex 时,Vue 根本不知道,所以视图不会更新。

  • 补救措施: 这就是为什么 Vue 2 需要 Vue.set(obj, 'sex', 'male')。它的作用就是手动补派一个保安过去。
为什么监听不到数组下标变化?

这是一个常见的误区。
技术上讲,Object.defineProperty 是可以 监听数组下标的(因为数组下标本质上就是 Key)。

// 技术上是可以做到的
Object.defineProperty(arr, '0', { ... }); 
Object.defineProperty(arr, '1', { ... });

但是,Vue 2 为什么放弃了这么做?
因为性能消耗太大了!
如果你有一个数组,里面有 10,000 个元素。Vue 就必须在初始化时循环 10,000 次,给每一个下标都加上 get/set。而且如果数组之后 push 了新元素,还得再给新元素加拦截。这会导致初始化巨慢无比。

所以,Vue 2 为了性能,选择了一个“权宜之计”:它重写(Monkey Patch) 了数组的 7 个方法(push, pop, shift, unshift, splice, sort, reverse)。

  • 当你调用 arr.push(1) 时,Vue 知道数组变了。
  • 但当你直接 arr[0] = 1 时,Vue 没办法拦截,只能“装瞎”。

2. Vue 3:全能的“大楼网关” (Proxy)

Proxy 不再是针对具体的“房间”,而是在大楼的门口设了一个唯一的总安检闸机

不管你是要去 name 房间,还是去 age 房间,甚至是去一个昨天还没建好、今天刚建好的 sex 房间,你都必须经过这个大门口。

let obj = { name: 'Gemini', arr: [1, 2] };

// Vue 3 的代理逻辑
const proxy = new Proxy(obj, {
    // 拦截读取
    get(target, key, receiver) {
        console.log(`监听到你正在读取: ${key}`);
        return Reflect.get(target, key, receiver);
    },
    // 拦截设置
    set(target, key, value, receiver) {
        console.log(`监听到你正在设置: ${key}${value}`);
        // 关键点:这里不需要关心 key 是否已经存在
        return Reflect.set(target, key, value, receiver);
    }
});
Proxy 如何解决那两个问题?
  1. 新增属性:
    当你执行 proxy.sex = 'male' 时,触发了 set 拦截器。此时参数 key 就是 'sex'Proxy 不需要提前知道 'sex' 存不存在,反正只要你往对象里塞东西,它就能拦截到。这就是为什么 Vue 3 不需要 Vue.set

  2. 数组下标变化:
    当你执行 proxy.arr[0] = 99 时:

    • 这也是一个 set 操作。
    • target 是数组。
    • key'0'
    • Proxy 会直接拦截到这次修改。
      不需要像 Vue 2 那样去循环遍历数组的每一个下标,Proxy 天生就能拦截对象(和数组)的所有操作。

3. 深度对比总结表

特性Object.defineProperty (Vue 2)Proxy (Vue 3)
拦截层级属性级 (针对具体的 Key)对象级 (针对整个 Object)
新增属性❌ 无法感知 (需用 Vue.set)✅ 天然感知 (触发 set 陷阱)
删除属性❌ 无法感知 (需用 Vue.delete)✅ 天然感知 (触发 deleteProperty 陷阱)
数组下标❌ 性能原因放弃支持 (无法拦截 arr[0]=1)✅ 天然支持
Map/Set❌ 不支持✅ 支持
兼容性✅ IE9+不兼容 IE (这是 Vue 3 最大的痛点)

4. 补充一个进阶点 (Reflect)

你在问题里提到了 get()set(),在 Vue 3 的源码中,Proxy 通常是和 Reflect 配合使用的(就像我上面的代码一样)。

为什么要用 Reflect?
主要是为了修正 this 的指向。
如果对象内部有 Getter 访问了 this.xxx,如果不配合 Reflect,这个 this 可能会指向原始对象而不是代理对象,导致依赖收集失败

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐