通过 Proxy、WeakMap 和 Dep 实现 reactive 的依赖收集、更新派发、重复代理处理和 ref 自动解包

2025-09-27 05:05:12

前面实现了 refeffect 以及依赖链表的更新流程。

这一篇开始 reactive。它和 ref 的区别在于:ref 可以把依赖直接挂在实例上,而 reactive 代理的是一个普通对象,对象里的每个属性都需要独立收集依赖。

基础结构

先写一个最小版本。

reactive 不直接创建 Proxy,而是交给 createReactiveObject 处理:

import { isObject } from '@vue/shared'

export function reactive(target: object) {
  return createReactiveObject(target)
}

function createReactiveObject(target: object) {
  if (!isObject(target)) {
    return target
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      trigger(target, key)
      return Reflect.set(target, key, value, receiver)
    }
  })

  return proxy
}

function track(target, key) {
}

function trigger(target, key) {
}

通过 createReactiveObject 包一层,是因为后面不只有 reactive,还会有 shallowReactive 这类不同的代理创建方式。

现在核心问题变成了:

  • get 时如何收集依赖
  • set 时如何派发更新

reactive 的依赖存在哪里

回到 ref 的实现,它是通过实例上的 subssubsTail 保存订阅者:

class Dep {
  subs: Link
  subsTail: Link
}

收集依赖时,只要拿到这个 dep,就可以继续复用之前的链表逻辑:

function track(target, key) {
  // if (activeSub) {
  //   link(dep, activeSub)
  // }
}

function trigger(target, key) {
  // if (dep.subs) {
  //   propagate(dep.subs)
  // }
}

问题在于,reactive 代理的是普通对象,不能像 ref 一样把 Dep 直接挂在实例上。

而且一个对象可能有多个属性,每个属性都要有自己的 Dep

const target = {
  a: 1,
  b: 2
}

所以需要一个额外的数据结构,把 targetkeyDep 关联起来。

使用 WeakMap

使用 WeakMap 保存对象和依赖之间的关系。

WeakMap 的特点是:

  • key 只能是对象,value 可以是任意类型
  • 它对 key 是弱引用
  • 如果一个对象只被 WeakMap 作为 key 引用,并且程序里没有其他地方再使用它,这个对象就可以被垃圾回收

如果使用普通 MapMap 会一直强引用 target。即使这个对象已经不再使用,只要它还在 Map 里,就不能被回收,容易造成内存泄漏。

最终结构是:

  • targetMap:全局 WeakMapkey 是原始对象 target
  • depsMap:每个对象对应一个 Mapkey 是对象属性
  • dep:每个属性对应一个 Dep,保存订阅这个属性的 effect

可以理解成下面这个结构:

targetMap = {
  [target]: {
    a: Dep,
    b: Dep
  }
}

这样就可以通过 target 找到对应的 depsMap,再通过属性名找到对应的 Dep

收集依赖

track 负责在读取属性时建立依赖关系。

function track(target: object, key: unknown) {
  if (!activeSub) {
    return
  }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Dep()))
  }

  link(dep, activeSub)
}

流程是:

  1. 如果当前没有正在执行的 effect,直接跳过
  2. 根据 targettargetMap 中取出 depsMap
  3. 如果不存在,就为这个对象创建一个新的 depsMap
  4. 根据 keydepsMap 中取出 dep
  5. 如果不存在,就为这个属性创建一个新的 Dep
  6. 调用 link(dep, activeSub) 建立订阅关系

派发更新

trigger 负责在属性修改时找到之前收集好的 dep,再通知订阅者更新。

function trigger(target: object, key: unknown) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }

  const dep = depsMap.get(key)
  if (!dep) {
    return
  }

  propagate(dep.subs)
}

更新流程和收集流程是对应的:

  1. 根据 target 找到 depsMap
  2. 根据 key 找到 dep
  3. 通过 propagate(dep.subs) 派发更新

reactive 的其他情况

避免重复代理

同一个原始对象多次传入 reactive 时,应该返回同一个代理对象:

it('同一个对象多次 reactive', () => {
  const obj = {
    a: 1
  }

  const state1 = reactive(obj)
  const state2 = reactive(obj)

  expect(state1).toBe(state2)
})

可以增加一个 reactiveMap,用来保存 target -> proxy 的关系:

const reactiveMap = new WeakMap()

function createReactiveObject(target: object) {
  const existProxy = reactiveMap.get(target)
  if (existProxy) {
    return existProxy
  }

  const proxy = new Proxy(target, {
    // ...
  })

  reactiveMap.set(target, proxy)
  return proxy
}

这样同一个原始对象只会创建一次代理。

处理代理对象再次传入

还需要处理另一种情况:已经是代理对象了,又被传入 reactive

it('代理对象再次传入 reactive', () => {
  const obj = {
    a: 1
  }

  const state1 = reactive(obj)
  const state2 = reactive(state1)

  expect(state1).toBe(state2)
})

如果不处理,就可能变成这样的结构:

state2 -> Proxy -> state1 -> Proxy -> obj

ref 可以通过自身的 IS_REF 标记判断是不是 refreactive 是通过 Proxy 实现的,也可以在代理的 get 拦截里返回一些特殊标记。

import type { Link } from './system'
import { isObject } from '@vue/shared'
import { activeSub } from './effect'
import { link, propagate } from './system'

const reactiveMap = new WeakMap<Target, any>()

export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  RAW = '__v_raw'
}

export function reactive(target: object) {
  return createReactiveObject(target)
}

function createReactiveObject(target: object) {
  if ((target as Target)[ReactiveFlags.RAW]) {
    return target
  }

  const existProxy = reactiveMap.get(target)
  if (existProxy) {
    return existProxy
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true
      }

      if (key === ReactiveFlags.RAW) {
        return target
      }

      track(target, key)
      return Reflect.get(target, key, receiver)
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

export interface Target {
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.RAW]?: any
}

const targetMap: WeakMap<Target, any> = new WeakMap<Target, any>()

export function isReactive(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

这里的 __v_isReactive__v_raw 并不是真实存在于对象上的属性,而是通过 Proxyget 拦截器返回的特殊值:

  • 读取 __v_isReactive 时返回 true
  • 读取 __v_raw 时返回原始对象 target

所以当一个代理对象再次传入 reactive 时,读取 ReactiveFlags.RAW 会触发代理对象的 get ,从而返回它对应的原始对象。这里就可以判断它已经是一个代理对象,直接返回当前值。

相同值不触发更新

如果给对象属性设置相同的值,不应该重复触发更新。

it('相同的值不触发更新', () => {
  const obj = {
    a: 1
  }

  const state = reactive(obj)
  const fn = vi.fn()

  effect(() => {
    state.a
    fn()
  })

  state.a = 1

  expect(fn).toBeCalledTimes(1)
})

set 中先保存旧值,真正写入后再比较新旧值:

set(target, key, value, receiver)
{
  const oldValue = target[key]
  const res = Reflect.set(target, key, value, receiver)

  if (hasChanged(oldValue, value)) {
    trigger(target, key)
  }

  return res
}

export function hasChanged(value: any, oldValue: any): boolean {
  return !Object.is(value, oldValue)
}

reactive 中自动解包 ref

ref 作为对象属性放进 reactive 时,读取属性时不需要再手动 .value

it('ref 嵌入 reactive', () => {
  const count = ref(0)
  const state = reactive({ count })
  const fn = vi.fn()

  effect(() => {
    state.count
    fn()
  })

  expect(state.count).toBe(0)

  state.count = 2

  expect(fn).toBeCalledTimes(2)
  expect(count.value).toBe(2)
})

这个测试希望满足三个行为:

  • 读取 state.count 时自动返回 count.value
  • 修改 state.count 后可以正常触发更新
  • 修改 state.count 后,原来的 count.value 也会同步更新

读取时可以在 get 中判断结果是不是 ref

get(target, key, receiver)
{
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true
  }

  if (key === ReactiveFlags.RAW) {
    return target
  }

  track(target, key)

  const res = Reflect.get(target, key, receiver)

  if (isRef(res)) {
    return res.value
  }

  return res
}

写入时,如果旧值是 ref,新值不是 ref,就不要替换整个属性,而是写入旧 ref.value

set(target, key, value, receiver)
{
  const oldValue = target[key]

  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }

  const res = Reflect.set(target, key, value, receiver)

  if (hasChanged(oldValue, value)) {
    trigger(target, key)
  }

  return res
}

这样 state.count = 2 实际更新的是 count.value,所以原来的 ref 对象仍然保持关联。

嵌套对象响应式

it('嵌套的 reactive 对象', () => {
  const obj = {
    a: {
      b: 1
    }
  }

  const state = reactive(obj)
  const fn = vi.fn()

  effect(() => {
    state.a.b
    fn()
  })

  state.a.b = 2

  expect(fn).toBeCalledTimes(2)
})

读取 state.a.b 时,第一次读取的是 state.a。如果 a 仍然只是一个普通对象,那么后续修改 a.b 时不会触发代理对象的 set

所以在 get 中,如果读取结果是对象,需要继续调用 reactive

get(target, key, receiver)
{
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true
  }

  if (key === ReactiveFlags.RAW) {
    return target
  }

  track(target, key)

  const res = Reflect.get(target, key, receiver)

  if (isRef(res)) {
    return res.value
  }

  if (isObject(res)) {
    return reactive(res)
  }

  return res
}

这样嵌套对象会在读取时被懒代理。只有真正访问到 state.a 时,才会把 a 转成响应式对象。