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 能实现新增属性监听、懒递归、精准依赖收集和更好的性能优化。

相关推荐
期待のcode2 小时前
Java中的this关键字
java·开发语言
前端早间课2 小时前
Vue3路由实战:优雅封装+灵活拦截,解锁路由配置新姿势
前端·javascript·vue.js
谅望者2 小时前
数据分析笔记15:Python模块、包与异常处理
开发语言·人工智能·python
黎雁·泠崖2 小时前
C 语言联合体与枚举:共用内存 + 常量枚举 + 实战
c语言·开发语言·python
yousuotu2 小时前
基于Python实现亚马逊销售数据分析与预测
开发语言·python·数据分析
L Jiawen2 小时前
【Golang基础】基础知识(上)
开发语言·后端·golang
bjzhang752 小时前
使用 HTML + JavaScript 实现级联选择器
前端·javascript·html
无知就要求知2 小时前
golang实现ftp功能简单又实用
java·前端·golang
卜锦元2 小时前
Golang后端性能优化手册(第四章:异步处理与消息队列)
开发语言·后端·docker·容器·性能优化·golang·团队开发