Proxy vs Object.defineProperty:Vue3响应式原理的深度革命

一、先下结论

Proxy 能做到 Object.defineProperty 做不到的事,根本原因不是"API 更高级",而是:

👉 Proxy 是"拦截整个对象的行为"

👉 defineProperty 是"劫持某一个已经存在的属性"

二、从语言底层:两者"拦截的是什么?"

1️⃣ Object.defineProperty ------「属性级劫持」

javascript 复制代码
Object.defineProperty(obj, 'a', {
  get() {},
  set() {}
})
能力 是否支持
拦截 get/set
拦截新增属性
拦截 delete
拦截 for...in / Object.keys
拦截数组索引变化 ❌(非常别扭)

👉 它只能拦截"已存在的 key 的读写"

本质上:

JS 引擎在访问 obj.a 时,会走你定义的 getter / setter

但如果是 obj.b = 1,引擎根本不会"通知你",因为你根本没劫持过 b

2️⃣ Proxy ------「对象级劫持(行为拦截)」⭐

javascript 复制代码
const proxy = new Proxy(obj, {
  get(target, key, receiver) {},
  set(target, key, value, receiver) {},
  deleteProperty(target, key) {},
  ownKeys(target) {}
})

Proxy 拦截的是:

"对这个对象做了什么操作"

而不是"访问了哪个属性"。

操作 是否能拦截
访问属性
修改属性
新增属性
删除属性
in 运算符
for...in / Object.keys
数组索引变化
length 变化

二、深层原理:为什么会有这种差异?

1. 设计哲学的差异

javascript 复制代码
// Object.defineProperty 的哲学:装饰模式
// 给现有属性添加额外的行为
function decorateProperty(obj, key) {
  let value = obj[key]
  
  Object.defineProperty(obj, key, {
    get() { return value },
    set(newVal) { 
      value = newVal 
      // 触发更新...
    }
  })
  
  return obj
}

// 问题:必须先有属性,才能装饰
const obj = {}
decorateProperty(obj, 'name')  // 必须先装饰
obj.name = '小明'  // 现在才会被监听

// Proxy 的哲学:代理模式
// 创建一个代理对象,拦截所有操作
function createProxy(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 在读取时才进行依赖收集
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      // 触发更新
      trigger(target, key)
      return true
    }
  })
}

// 优势:拦截先行,操作后置
const obj = {}
const proxy = createProxy(obj)  // 先创建代理
proxy.name = '小明'  // 操作时自动被拦截

三、为什么 defineProperty 监听不了「新增属性」?

obj.b = 1

JS 引擎内部做了什么?

  1. 检查 obj 是否已有 b

  2. 如果没有 → 直接创建属性

  3. 赋值

👉 整个过程没有任何"属性级回调钩子"

Object.defineProperty 的 getter/setter 只在:

obj.b // 读取已定义属性

obj.b = // 写入已定义属性

才生效。

Vue2 是怎么"硬补"的?

Vue.set(obj, 'b', 1)

本质:defineReactive(obj, 'b', 1)

👉 手动补一个 defineProperty

⚠️ 这也是 Vue2 最大的心智负担之一。

四、Proxy 为什么天然支持「新增属性监听」?

proxy.b = 1

流程变成:

  1. 触发 Proxy 的 set trap

  2. set trap 里你可以判断:

    const hadKey = Object.hasOwn(target, key)

  3. 决定是:

    • 新增 → trigger ADD

    • 修改 → trigger SET

      因为 Proxy 拦截的是"赋值行为"本身

      而不是属性。

五、核心难点二:什么叫「按需响应式」?

Vue2 的问题:全量递归劫持

javascript 复制代码
data() {
  return {
    a: {
      b: {
        c: 1
      }
    }
  }
}

Vue2 在初始化时会:

walk(a)

walk(a.b)

walk(a.b.c)

👉 不管你用不用,全部 defineProperty 一遍

Vue3 的思路:用的时候再劫持

javascript 复制代码
const state = reactive({
  a: {
    b: {
      c: 1
    }
  }
})

只有当你:state.a.b.c

执行流程是:

get(state, 'a') → 返回 proxy(a)

get(proxy(a), 'b') → 返回 proxy(b)

get(proxy(b), 'c') → track

六、从"响应系统设计"角度对比

Vue2(defineProperty)

初始化阶段:

全量 walk

每个 key 都有一个 Dep

特点:

  • 依赖关系 = 属性级

  • 初始化成本极高

  • 无法精确拦截结构变化

Vue3(Proxy)

运行阶段:

用到哪个 key

才 track 哪个 key

特点:

  • 依赖关系 = target + key

  • 没访问 → 没依赖

  • 没依赖 → 不触发更新

Object.defineProperty 是"属性级劫持",只能劫持已存在的 key,因此无法监听新增、删除和结构性变化,也无法按需响应;而 Proxy 是"对象级行为拦截",可以拦截所有对对象的操作,使得 Vue3 能实现新增属性监听、懒递归、精准依赖收集和更好的性能优化。

相关推荐
C_心欲无痕34 分钟前
前端如何实现 [记住密码] 功能
前端
沐知全栈开发6 小时前
Perl 数据库连接
开发语言
森叶6 小时前
Java 比 Python 高性能的原因:重点在高并发方面
java·开发语言·python
qq_316837756 小时前
uni.chooseMedia 读取base64 或 二进制
开发语言·前端·javascript
方圆工作室6 小时前
【C语言图形学】用*号绘制完美圆的三种算法详解与实现【AI】
c语言·开发语言·算法
Zoey的笔记本7 小时前
2026告别僵化工作流:支持自定义字段的看板工具选型与部署指南
大数据·前端·数据库
小二·7 小时前
Python Web 开发进阶实战:混沌工程初探 —— 主动注入故障,构建高韧性系统
开发语言·前端·python
Lkygo7 小时前
LlamaIndex使用指南
linux·开发语言·python·llama
进阶小白猿7 小时前
Java技术八股学习Day20
java·开发语言·学习
gis开发7 小时前
【无标题】
java·前端·javascript