Vue 3 响应式系统源码剖析:Proxy 与 Reflect 如何协同工作

Vue 3 最引人注目的架构升级之一,就是用 Proxy 重写了整个响应式系统。相比 Vue 2 基于 Object.defineProperty 的实现,Proxy 带来了更强大的拦截能力、更自然的语法以及更好的性能表现。而在 Proxy 的背后,Reflect 扮演着同样关键的角色。本文将深入 Vue 3 源码,剖析 ProxyReflect 是如何协同工作,构建出这套高效响应式系统的。

为什么 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
  }
}

可以看到,getset 中都是先用 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 变化、adddelete 等操作做了差异化处理,确保触发逻辑精准。

惰性递归与缓存

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 如何协同工作

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏