人人都会AI编程

4.2 Vue 3 响应式:Proxy + Reflect 实现

更新时间:2026-07-09

Vue 2 的响应式系统基于 Object.defineProperty,存在几个无法绕开的痛点:无法监听属性的添加和删除、对数组的索引和 length 修改无能为力、初始化时需要递归遍历所有属性进行劫持。Vue 3 用 ES6 的 Proxy 搭配 Reflect 重写了整个响应式,一次性解决了这些问题,并且带来了更简洁的实现和更好的性能。

Proxy 代理的核心优势

Proxy 可以直接代理一整个对象,而不是对象的某个属性。无论你访问、修改、删除这个对象上的任意属性,甚至访问原型链上的属性,Proxy 都能拦截到。

用一个最简单的例子感受区别:

// Vue 2:只能劫持预先存在的属性
const obj = {}
Object.defineProperty(obj, 'a', { get() {...}, set() {...} })
obj.a = 1  // ✅ 触发 set
obj.b = 2  // ❌ 无法触发 set,因为 b 未预先定义

// Vue 3:代理整个对象
const proxy = new Proxy({}, {
  get(target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})
proxy.a = 1  // ✅ 触发 set(key='a')
proxy.b = 2  // ✅ 触发 set(key='b')

这个特性直接解决了 Vue 2 中“新增属性不响应”和“数组索引修改不生效”的问题。此外,Proxy 能拦截的远不止 getset,它原生支持 13 种操作,包括 has(in 操作符)、deleteProperty(delete 操作符)、ownKeys(Object.keys 等)——这让 Vue 3 可以实现对 delete obj.name 的自动响应,不再需要 Vue.delete

深层响应式与浅层响应式原理

Vue 3 提供了 reactiveshallowReactive,分别对应深层响应式和浅层响应式。

深层响应式(reactive)
当你用 reactive 包裹一个对象时,Vue 并不是在初始化阶段就把对象里所有嵌套属性全部劫持。它采用了一种“懒代理”策略:

  1. 返回的 Proxy 只拦截当前对象层级的操作。
  2. 当你读取某个属性(如 obj.nested.data),get 拦截器会被触发。
  3. 如果读取到的值是一个对象类型(且不是已经代理过的),Vue 会在这个时刻递归调用 reactive 将它也包成 Proxy,并缓存结果。
  4. 修改 obj.nested.data = 5 时,内层 Proxy 会独立触发更新。

这样既保证了所有深层访问都能自动变为响应式,又避免了初始化时对大量暂未用到的嵌套数据的无意义劫持。

浅层响应式(shallowReactive)
shallowReactive 则只在第一层做 Proxy 代理。嵌套对象本身不会被自动转为响应式代理,访问到的还是一个普通对象。这在你明确只需要追踪一层变化,或者有大量静态嵌套数据时,可以减少代理开销。

const state = shallowReactive({
  info: { name: 'Vue' }
})
state.info.name = 'React'  // ❌ 不会触发视图更新,因为 info.name 不在代理范围内
state.info = { name: 'React' } // ✅ 会触发更新,因为替换了第一层的 info 属性

ref 的内部实现也遵循类似的逻辑:如果传入的是对象,ref 会调用 reactive 将其转为深层响应式;如果是基本类型,则通过 getter/setter 绑定一个内部的 .valueshallowRef 则直接暴露内部值,对 .value 是对象的情况不做深层代理。

数组与对象的统一代理机制

Vue 2 为了监听数组变化,不得不重写了能改变数组自身的 7 个方法(pushpopshiftunshiftsplicesortreverse),通过拦截这些方法的调用来派发更新。这带来了两个问题:第一,直接用 arr[0] = 1arr.length = 0 无法触发响应;第二,调用某些数组方法(如 concatslice)时,返回的新数组是否响应式也需要特殊处理。

Vue 3 的 Proxy 不再需要任何数组方法重写。因为 arr[0] = 1 本质上是对数组对象执行 set 操作,Proxy 能像拦截普通对象属性一样拦截数组的索引赋值。arr.length = 0 同样会被 set 捕获。数组方法内部对 length 或索引的修改也会自然触达 set 拦截器。整个过程对对象和数组是完全统一的——它们都是对 Proxy 的操作,开发者不需要记忆任何“数组响应式的特殊规则”。

const list = reactive([1, 2, 3])

list.push(4)   // ✅ 自动触发视图更新
list[0] = 99   // ✅ 自动触发视图更新(Vue 2 中无效)
list.length = 0 // ✅ 自动触发视图更新(Vue 2 中无效)

Reflect 在这里扮演了一个稳定的配合角色。Proxy 的拦截函数里,Vue 使用 Reflect.get(target, key, receiver)Reflect.set(target, key, value, receiver) 来确保 this 指向正确,并且返回值能准确反映操作是否成功(比如 Reflect.set 在目标属性不可写时会返回 false)。这也让响应式系统在边界情况下更健壮,不易出现因 this 丢失导致的静默错误。

总结 Vue 3 响应式的真实提升

  • 不再需要 Vue.setVue.delete 来添加/删除属性。
  • 数组索引修改、length 变更直接响应,无需学习数组方法的特殊限制。
  • 初始化时不需要递归劫持所有深层数据,仅在实际访问时惰性代理,启动更快,内存占用更低。
  • 代码实现更简洁,避免了 Vue 2 中复杂的数组方法拦截与依赖收集逻辑,维护性大幅提高。

这一改进是 Vue 3 性能与开发体验提升的基石之一,也从根本上消解了 Vue 2 开发者常吐槽的“响应式失效”问题。