在 Vue 2 时代,响应式系统的核心是 Object.defineProperty。它对对象的处理还算顺畅,但面对数组时却遇到了不小的麻烦:Object.defineProperty 只能劫持对象的属性,而数组经常通过索引直接修改元素(arr[0] = newVal)、或者改变长度(arr.length = 0),这些操作都无法触发视图更新。Vue 2 不得不采用重写数组原型方法的方式(push、pop、shift 等)来让这些变更可感知——但这导致了一个棘手的割裂:对象和数组遵循两套完全不同的响应式处理逻辑。
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')和修改 length(arr.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 不再通过重写数组方法来打补丁,但你要知道:在代理数组上调用 push、pop、splice 等方法时,响应式系统依然能正常工作。根本原因在于这些方法内部会触发属性的读取和设置操作,而 Proxy 都会拦截到。
例如 proxyArr.push(5) 的执行过程:
- 读取
push方法(触发get拦截,对应key为 "push") - 读取
length属性(触发get拦截,用于内部计算) - 设置新的索引
proxyArr[原length] = 5(触发set拦截,依赖更新在此派发) - 更新
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(...) 也能响应。这种“想怎么写就怎么写”的自由度,正是统一代理机制带来的最大红利。