Vue 3 最引人注目的架构升级之一,就是用 Proxy 重写了整个响应式系统。相比 Vue 2 基于 Object.defineProperty 的实现,Proxy 带来了更强大的拦截能力、更自然的语法以及更好的性能表现。而在 Proxy 的背后,Reflect 扮演着同样关键的角色。本文将深入 Vue 3 源码,剖析 Proxy 与 Reflect 是如何协同工作,构建出这套高效响应式系统的。
为什么 Vue 3 选择 Proxy
在理解二者如何协同之前,先回顾 Vue 2 的痛点。Object.defineProperty 存在几个根本性限制:
- 无法监听属性的新增和删除:Vue 2 只能劫持已存在的属性,新增属性需要用
Vue.set手动处理。 - 无法直接监听数组索引和 length 的变化:Vue 2 通过重写数组的七个方法(push、pop 等)来打补丁。
- 需要递归遍历所有属性:初始化时就要深度遍历对象,性能开销大。
- 只能劫持对象的属性,不能劫持对象本身。
Proxy 则是在对象层面建立拦截,天然支持新增、删除、in 操作符、数组索引等十多种操作,且支持惰性递归——只有真正访问到嵌套对象时才进行代理,性能更优。
Proxy 与 Reflect 的分工
在 Vue 3 的 baseHandlers.ts 中,核心的拦截器由两部分组成:Proxy 负责“拦截”,Reflect 负责“执行默认行为”。
const mutableHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, TrackOpTypes.GET, key)
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 触发更新
if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
}
可以看到,get 和 set 中都是先用 Reflect 执行原始操作,再附加依赖收集或触发更新的逻辑。这就是二者协同的基础模式:Proxy 提供拦截时机,Reflect 保证原始语义不被破坏。
为什么必须用 Reflect
如果只是简单地在 get 中返回 target[key],在 set 中写 target[key] = value,程序似乎也能跑。那为什么 Vue 3 坚持用 Reflect?原因主要有三点。
1. 保证 receiver 的正确传递
当代理对象存在继承关系时,receiver 决定了 this 的指向。考虑下面这段代码:
const parent = { get value() { return this._value } }
const child = Object.create(parent)
const proxy = new Proxy(child, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
}
})
使用 Reflect.get(target, key, receiver) 时,getter 内部的 this 会正确指向 receiver(即代理对象)。如果写成 target[key],this 会指向原始 target,导致后续访问无法被代理捕获,响应式链路断裂。
2. 返回值语义更严谨
Reflect 的方法返回值与操作结果严格对应。例如 Reflect.set 会返回布尔值表示是否设置成功,而直接赋值在严格模式下失败会抛错。这种一致性让拦截器逻辑更清晰。
3. 与 Proxy 的 trap 一一对应
Proxy 的每个 trap 都能在 Reflect 中找到同名方法。这种对称设计让代码更易读、更易维护,也避免了手动实现默认行为时可能引入的细微 bug。
依赖收集与触发更新
理解了 Proxy + Reflect 的协作模式,再看响应式的完整闭环就清晰了。
在 get 拦截中,Vue 3 调用 track 进行依赖收集。它会将当前活跃的副作用函数(activeEffect)与目标对象的某个 key 建立关联,存储在全局的 targetMap 中:
targetMap: WeakMap<target, Map<key, Dep>>
在 set 拦截中,Vue 3 调用 trigger 触发更新。它从 targetMap 中取出该 key 对应的所有副作用函数,逐一执行。
值得注意的是,Vue 3 使用 hasChanged 做值比较,避免相同值重复触发更新;同时针对数组的 length 变化、add 和 delete 等操作做了差异化处理,确保触发逻辑精准。
惰性递归与缓存
Vue 3 的 reactive 还有两个精妙设计:
- 惰性代理:
get中只有res是对象时才调用reactive(res),这意味着深层嵌套对象只有被访问时才代理,初始化成本大幅降低。 - 代理缓存:通过
WeakMap缓存 target 到 proxy 的映射,避免对同一对象重复创建代理,同时借助 WeakMap 的弱引用特性防止内存泄漏。
const proxyMap = new WeakMap()
function reactive(target) {
if (proxyMap.has(target)) return proxyMap.get(target)
const proxy = new Proxy(target, mutableHandlers)
proxyMap.set(target, proxy)
return proxy
}
总结
Vue 3 响应式系统的核心可以概括为一句话:Proxy 负责“在正确的时机拦截”,Reflect 负责“以正确的方式执行”。二者配合,既保证了原始对象语义的完整性,又为依赖收集和触发更新提供了精准的切入点。
相比 Vue 2,这套设计不仅解决了对象新增/删除、数组索引等历史难题,还通过惰性递归和缓存机制实现了性能优化。理解 Proxy 与 Reflect 的协同原理,是读懂 Vue 3 响应式源码、乃至手写一个 mini-reactive 的关键一步。
未经允许不得转载:任鹏个人博客 » Vue 3 响应式系统源码剖析:Proxy 与 Reflect 如何协同工作


朋友圈点赞图在线生成源码