人人都会AI编程

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

更新时间:2026-07-11

在 Vue 2 时代,响应式系统的核心是 Object.defineProperty。它对对象的处理还算顺畅,但面对数组时却遇到了不小的麻烦:Object.defineProperty 只能劫持对象的属性,而数组经常通过索引直接修改元素(arr[0] = newVal)、或者改变长度(arr.length = 0),这些操作都无法触发视图更新。Vue 2 不得不采用重写数组原型方法的方式(pushpopshift 等)来让这些变更可感知——但这导致了一个棘手的割裂:对象和数组遵循两套完全不同的响应式处理逻辑。

Vue 3 将响应式基石换成了 ES6 的 Proxy + Reflect,彻底消灭了这种割裂。Proxy 能够直接代理整个对象(包括数组)的多种操作,不再区分数据类型,这让对象和数组在响应式行为上获得了统一且完整的支持。

Proxy 为何能统一处理对象和数组

Proxy 不是对“属性”进行劫持,而是对目标对象的操作行为进行拦截。它提供了 13 种拦截器(Handler trap),覆盖了对目标的各种操作,包括:

  • get(target, key, receiver) —— 拦截属性读取
  • set(target, key, value, receiver) —— 拦截属性设置(包括数组新增元素和修改索引)
  • deleteProperty(target, key) —— 拦截属性删除
  • has(target, key) —— 拦截 in 运算符
  • 等等……

对于数组,set 拦截器能够捕获通过索引赋值arr[2] = 'x')和修改 lengtharr.length = 0)的操作。这意味着 Vue 3 不再需要重写数组方法,也可以让这些操作自动触发依赖更新。开发者可以像操作普通变量一样自由地修改数组,响应式系统完全感知。

代码示例:Proxy 包裹一个简单的数组

const rawArray = [1, 2, 3]

const proxy = new Proxy(rawArray, {
  get(target, key, receiver) {
    console.log(`读取索引 ${key}`)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(`设置索引 ${key} 为 ${value}`)
    return Reflect.set(target, key, value, receiver)
  }
})

proxy[1]       // 控制台:读取索引 1   → 返回 2
proxy[3] = 4   // 控制台:设置索引 3 为 4
proxy.length   // 控制台:读取索引 length → 返回 4

这个代理不区分 target 是对象还是数组,get/set 对所有属性访问和修改一视同仁。

Reflect 的作用:保持默认行为,避免副作用

Vue 3 响应式实现中大量使用 Reflect 方法配合 Proxy。Reflect 提供与 Proxy 拦截器一一对应的静态方法,它的作用是执行原始操作并返回标准结果。比如 Reflect.get(target, key, receiver) 会触发 target[key] 的读取,同时保证 this 绑定正确(receiver 参数确保了代理对象上的访问器属性中的 this 指向代理本身,而非原始对象)。使用 Reflect 而不是直接操作原始对象,能够避免一些边界情况下的 bug,也让代码更规范。

数组方法的响应式行为

虽然 Vue 3 不再通过重写数组方法来打补丁,但你要知道:在代理数组上调用 pushpopsplice 等方法时,响应式系统依然能正常工作。根本原因在于这些方法内部会触发属性的读取和设置操作,而 Proxy 都会拦截到

例如 proxyArr.push(5) 的执行过程:

  1. 读取 push 方法(触发 get 拦截,对应 key 为 "push")
  2. 读取 length 属性(触发 get 拦截,用于内部计算)
  3. 设置新的索引 proxyArr[原length] = 5(触发 set 拦截,依赖更新在此派发)
  4. 更新 length 值(触发 set 拦截,key 为 "length")

整个过程中,Vue 的响应式系统会自动跟踪依赖并在 set 时通知更新,开发者完全无需关心背后的机制。

统一代理带来的实际好处

  • 心智负担更小:不需要记“数组要通过 $set 或者 splice 才能触发更新”,直接 arr[0] = newValue 就行,对象和数组的用法完全对齐。
  • 减少隐藏 Bug:Vue 2 中因为忘记使用 Vue.set 或数组合法的变异方法而导致页面不刷新的坑,Vue 3 中彻底消失。
  • 深层响应式仍然自动reactive 默认会递归地将嵌套对象和数组都变成响应式,因为 Proxy 在 get 时判断返回值是对象则继续包装——数组元素如果是对象,也会被顺滑地代理。

真实开发中最直观的变化:以前在 Vue 2 里,如果要清空一个响应式数组并追加新数据,稳妥的写法可能是 arr.splice(0, arr.length, ...newItems),而现在你可以直接写 arr = reactive([]) 然后随意操作,甚至 arr.length = 0; arr.push(...) 也能响应。这种“想怎么写就怎么写”的自由度,正是统一代理机制带来的最大红利。