Skip to content

reactive 与 readonly:响应式对象是如何被代理的?

Vue 3 的响应式系统是其核心:当我们修改一个数据时,视图就自动更新。这种响应式的系统在 Vue 3 中被重写,它放弃了 Vue 2 的 Object.defineProperty,转而使用 ES6 的 Proxy

这不仅是一次内部实现的替换,更是一次设计思想上的升级。本节,我们将深入 reactive 和 readonly 的源码,分析这个代理机制的实现原理。


为什么选择 Proxy?一场“监控”的革命

在 Vue 2 中,Object.defineProperty 来实现响应式,但这存在一些固有的技术局限性:

javascript
// Vue 2 的响应式
Object.defineProperty(obj, 'a', {
  get() { /* ... */ },
  set() { /* ... */ }
})

这个非常管用,但有致命缺陷:

  • 无法监测属性的添加”:对于后续添加的属性(如 obj.b = 2),你必须手动将其转换为响应式(Vue.set)。
  • 无法监测属性的删除”:你无法检测到 delete obj.a
  • 数组的局限性:你无法监控 arr[0] = 4 这种索引访问,Vue 2 只能通过“重写”push, pop 等七个数组方法来妥协。

Proxy 则完全不同。它在目标对象之上架设了一个拦截层。。

javascript
// Vue 3 的“万能门卫”
const p = new Proxy(obj, {
  get() { /* ... */ },
  set() { /* ... */ },
  deleteProperty() { /* ... */ },
  has() { /* ... */ }
  // ...拦截 13 种操作
})

Proxy 可以拦截目标对象上的所有基本操作。无论是添加新属性、删除属性还是修改数组索引,都会被 Proxy 的 handler(处理器)捕获。

这从根本上解决了 Vue 2 响应式系统的所有局限性。


createReactiveObject:响应式对象的创建流程

当我们调用 reactive(obj) 时,我们并不是在直接创建 Proxy。Vue 在背后调用了一个核心工厂函数:createReactiveObject

这个函数非常严谨,它不是直接创建代理,而是进行了一系列精密的检查和决策:

typescript
// core/packages/reactivity/src/reactive.ts
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>, // 代理的“规则手册”
  collectionHandlers: ProxyHandler<any>, // 针对 Map/Set 的特殊规则
  proxyMap: WeakMap<Target, any>, // “访客登记表”
) {
  // 1. 合法性检查:只能代理对象和数组
  if (!isObject(target)) {
    return target
  }

  // 2. 防御:如果 target 已经是 Proxy,直接返回
  // (除非是 readonly 想代理一个 reactive)
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 3. 缓存检查:这个“代理”是否已经存在?
  // 核心!避免对同一个对象重复创建代理
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 4. 合法性检查:是否是 VUE_SKIP 或不可扩展对象?
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 5. 创建!
  const proxy = new Proxy(
    target,
    // 普通对象用 baseHandlers,Map/Set 用 collectionHandlers
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )

  // 6. 登记缓存
  proxyMap.set(target, proxy)
  return proxy
}

这个流程展示了 Vue 3 响应式系统的健壮性高性能。其中最精妙的是第 3 步。


WeakMap 缓存:避免重复代理

想象一下,如果你在一个函数里调用了 reactive(obj),在另一个函数里又调用了一次 reactive(obj),Vue 应该创建两个不同的“代理”吗?

绝对不行。这会导致 reactive(obj) === reactive(obj) 的结果为 false,引发灾难性的 bug。

Vue 的解决方案是使用 WeakMap 作为缓存

typescript
// core/packages/reactivity/src/reactive.ts
export const reactiveMap: WeakMap<Target, any> = new WeakMap()
export const readonlyMap: WeakMap<Target, any> = new WeakMap()
// ... 还有 shallowReactiveMap 和 shallowReadonlyMap
  • 键(Key):原始的 target 对象。
  • 值(Value):已经创建好的 proxy 对象。

createReactiveObject 运行时,它会先查这个缓存表(proxyMap.get(target))。如果查到了,就直接返回,保证了同一个原始对象永远只对应唯一一个代理

为什么用 WeakMap 而不是 MapWeakMap 对键是“弱引用”。如果你的原始 target 对象在别处被销毁、垃圾回收了,WeakMap 中的缓存记录也会自动消失,从而防止内存泄漏。


handlers:Proxy 的拦截逻辑

Proxy 的第二个参数 handlers 是定义拦截逻辑的地方。Vue 3 定义了四套 handlers

  1. mutableHandlers:用于 reactive()(可读可写)。
  2. readonlyHandlers:用于 readonly()(只读)。
  3. shallowReactiveHandlers:用于 shallowReactive()
  4. shallowReadonlyHandlers:用于 shallowReadonly()

reactivereadonlyhandlers 差异主要体现在 getset 拦截上。

mutableHandlers (可读可写处理器)

这是 reactive 使用的核心处理器,我们重点看 getset

get(target, key, receiver):读取拦截

当代码执行 state.user 时,get 处理器被触发:

  1. 内部标记检查:检查 key 是否是 ReactiveFlags.IS_REACTIVEReactiveFlags.RAW 等内部标记。如果是,返回相应的值(例如 truetarget 本身)。
  2. 执行读取const res = Reflect.get(target, key, receiver)。获取原始值。
  3. 依赖收集track(target, TrackOpTypes.GET, key)这是关键:调用 track 函数,通知响应式系统:“有人正在读取这个属性,请记录下来。”(track 的实现我们将在 3.3 节分析)。
  4. 【核心】按需(Lazy)递归代理
    typescript
    if (isObject(res)) {
      // 如果读取到的值 (res) 也是一个对象,
      // 则递归地将其也转换为 reactive (或 readonly)
      return isReadonly ? readonly(res) : reactive(res)
    }
    这是 Vue 3 的一项重要性能优化。reactive 并不会在创建时立即递归代理所有嵌套对象。它只在嵌套对象被第一次访问(get)时,才“按需”将其转换为 reactive

set(target, key, value, receiver):写入拦截

当代码执行 state.count = 1 时,set 处理器被触发:

  1. 执行写入const result = Reflect.set(target, key, value, receiver)。先让原始的写入操作生效。
  2. 派发更新trigger(target, TriggerOpTypes.SET, key, value, oldValue)这是关键:调用 trigger 函数,通知响应式系统:“这个属性被修改了,请通知所有订阅者更新。”(trigger 的实现我们将在 3.3 节分析)。
  3. 返回 result(写入是否成功)。

readonlyHandlers (只读处理器)

readonly 的处理器逻辑很简单。它的 get 处理器与 mutableHandlers 类似(同样需要 track 和递归 readonly(res))。

但它的 setdeleteProperty 处理器则会“拦截”所有写入和删除操作:

typescript
// core/packages/reactivity/src/baseHandlers.ts
class ReadonlyReactiveHandler extends BaseReactiveHandler {
  set(target: object, key: string | symbol) {
    if (__DEV__) {
      // 在开发环境,发出警告
      warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target,
      )
    }
    return true // 操作静默失败
  }

  deleteProperty(target: object, key: string | symbol) {
    if (__DEV__) {
      warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target,
      )
    }
    return true // 操作静默失败
  }
}

集合类型的特殊处理 (Map/Set)

Proxy 无法拦截 map.get()map.set() 这种方法调用的内部执行。

因此,Vue 3 专门为 MapSetWeakMapWeakSet 提供了 collectionHandlers。它不拦截 getset,而是重写了 getsetdeletehasforEach 等方法

collectionHandlers 拦截到对 map.get(key)方法调用时,它会:

  1. 正常执行 target.get(key) 获取 value
  2. 手动调用 track() 收集依赖。
  3. 如果 value 是对象,也递归地将其变为 reactivereadonly
  4. map.set(key, value) 同理,会在执行后手动调用 trigger()

总结

reactivereadonly 的实现是 Vue 3 响应式系统的基石。它围绕 Proxy 构建了一个健壮且高效的代理系统:

  1. Proxy 替代:使用 Proxy 提供了完整的 13 种操作拦截能力,解决了 Object.defineProperty 的所有局限性。
  2. createReactiveObject (工厂):作为统一入口,负责处理类型检查和缓存。
  3. WeakMap (缓存):通过 WeakMap 缓存已创建的代理,保证了对象引用的唯一性,并防止内存泄漏。
  4. Handlers (处理器)
    • get 拦截中:调用 track() (收集依赖),并按需递归处理嵌套对象(性能优化)。
    • set 拦截中:调用 trigger() (派发更新)。
    • readonly 处理器则会阻止 setdeleteProperty 操作。

微信公众号二维码

Last updated: