Skip to content

实现响应式基础 - reactive

本节对标 Vue 3 源码 @vue/reactivity 中的 reactive.ts

什么是响应式?

响应式的本质是:当数据变化时,自动执行依赖这些数据的副作用函数。

Vue 2 使用 Object.defineProperty 实现响应式,存在以下局限:

  • 无法检测属性的添加和删除(需要 Vue.set / Vue.delete
  • 无法检测数组索引和 length 的变化
  • 需要递归遍历对象的所有属性

Vue 3 使用 Proxy 重写了整个响应式系统,从根本上解决了这些问题。

核心数据结构

Vue 3 响应式的依赖收集基于一个三层 Map 结构:

targetMap: WeakMap<target, depsMap>
    └── depsMap: Map<key, dep>
            └── dep: Set<ReactiveEffect>
  • targetMap:以原始对象为 key,存储该对象所有属性的依赖映射
  • depsMap:以属性名为 key,存储该属性的所有依赖
  • dep:一个 Set,存储所有依赖该属性的 effect 函数

WeakMap 是因为当原始对象被垃圾回收时,对应的依赖映射也会自动清理。

ts
// 依赖集合类型:存储所有依赖某个属性的 effect 函数
type Dep = Set<ReactiveEffect>
// 属性到依赖集合的映射类型:一个对象的每个属性都有自己的依赖集合
type KeyToDepMap = Map<any, Dep>
// 全局依赖映射表:使用 WeakMap 以原始对象为 key,当对象被回收时对应的依赖映射也会被自动清理,避免内存泄漏
const targetMap = new WeakMap<any, KeyToDepMap>()

实现 reactive

reactive 的核心是使用 Proxy 包装原始对象,拦截 get 和 set 操作:

ts
// 缓存已创建的 proxy,用 WeakMap 确保同一对象只创建一个代理,且不影响垃圾回收
const reactiveMap = new WeakMap<object, any>()

export function reactive<T extends object>(target: T): T {
  // 先检查该对象是否已经有对应的 proxy,如果有则直接返回,避免重复代理
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 使用 Proxy 包装原始对象,拦截属性的读取、设置和删除操作
  const proxy = new Proxy(target, {
    // get 拦截器:在读取属性时触发依赖收集
    get(target, key, receiver) {
      // 当访问 __v_isReactive 标识时,返回 true,用于 isReactive() 判断
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true
      }
      // 当访问 __v_raw 标识时,返回原始对象,用于 toRaw() 获取原始值
      if (key === ReactiveFlags.RAW) {
        return target
      }

      // 使用 Reflect.get 读取属性值,传递 receiver 确保 getter 中 this 指向代理对象
      const res = Reflect.get(target, key, receiver)

      // 依赖收集:将当前正在执行的 effect 添加到该属性的依赖集合中
      track(target, key)

      // 如果属性值是对象,递归转换为响应式(惰性转换:只在访问时才转换,而非初始化时遍历所有属性)
      if (isObject(res)) {
        return reactive(res)
      }

      return res
    },
    // set 拦截器:在设置属性时触发依赖更新
    set(target, key, value, receiver) {
      // 记录旧值,用于后续比较判断是否需要触发更新
      const oldValue = (target as any)[key]
      // 使用 Reflect.set 设置属性值
      const result = Reflect.set(target, key, value, receiver)

      // 只有在值真正发生变化时才触发更新,避免不必要的重复渲染
      if (oldValue !== value) {
        // 触发更新:通知所有依赖该属性的 effect 重新执行
        trigger(target, key)
      }

      return result
    },
    // deleteProperty 拦截器:在删除属性时触发依赖更新(这是 Vue 3 相比 Vue 2 的改进之一)
    deleteProperty(target, key) {
      // 检查属性是否真的存在于该对象上(不包括原型链上的属性)
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      // 执行删除操作
      const result = Reflect.deleteProperty(target, key)

      // 只有属性确实存在且删除成功时,才触发更新
      if (hadKey && result) {
        trigger(target, key)
      }

      return result
    },
  })

  // 将创建好的 proxy 缓存起来,下次对同一对象调用 reactive 时直接返回
  reactiveMap.set(target, proxy)
  return proxy as T
}

关键设计决策

1. 惰性递归(Lazy Reactive)

Vue 2 在初始化时递归遍历所有属性进行 defineProperty,性能开销大。

Vue 3 采用惰性转换 —— 只在 get 访问到深层对象时才递归调用 reactive

ts
// 只有真正访问到 obj.nested 时,nested 才会被转换为响应式
if (isObject(res)) {
  return reactive(res)
}

2. Reflect 而不是直接操作

使用 Reflect.get / Reflect.set 而不是 target[key],是因为需要正确传递 receiver(即 proxy 本身),确保 getter/setter 中的 this 指向 proxy 而非原始对象。

3. 同一对象只创建一个 proxy

通过 reactiveMap 缓存,避免对同一个对象重复创建 proxy:

ts
const reactiveMap = new WeakMap<object, any>()

// 如果已经存在 proxy,直接返回
const existingProxy = reactiveMap.get(target)
if (existingProxy) {
  return existingProxy
}

实现 track(依赖收集)

trackget 拦截器中调用,将当前正在执行的 effect 添加到对应属性的依赖集合中:

ts
// 当前正在执行的 effect 实例,track 时通过它来建立依赖关系
let activeEffect: ReactiveEffect | undefined

export function track(target: object, key: unknown) {
  // 如果没有正在执行的 effect,说明当前不在响应式上下文中,无需收集依赖
  if (!activeEffect) 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 Set()))
  }

  // 避免重复收集:只有当前 effect 还没被收集到该属性的依赖中时才添加
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect) // 将当前 effect 添加到属性的依赖集合中(属性 → effect 的映射)
    activeEffect.deps.push(dep) // 让 effect 也反向记录自己被哪些 dep 收集了,便于后续清理无效依赖
  }
}

activeEffect.deps.push(dep) 这行很关键 —— 让 effect 也记住自己被哪些 dep 收集了,后续清理依赖时会用到。

实现 trigger(触发更新)

triggerset 拦截器中调用,找到对应属性的所有依赖 effect 并执行:

ts
export function trigger(target: object, key: unknown) {
  // 获取该对象的依赖映射表
  const depsMap = targetMap.get(target)
  if (!depsMap) return // 该对象从未被 track 过,无需触发

  // 获取该属性对应的依赖集合
  const dep = depsMap.get(key)
  if (!dep) return // 该属性没有任何依赖,无需触发

  // 复制一份依赖集合,避免在遍历过程中因 effect 执行导致原集合变化引发无限循环
  const effects = new Set<ReactiveEffect>()
  dep.forEach((effect) => {
    // 避免 effect 在自己的执行过程中重复触发(防止无限递归)
    if (effect !== activeEffect) {
      effects.add(effect)
    }
  })

  // 遍历执行所有依赖的 effect
  effects.forEach((effect) => {
    if (effect.scheduler) {
      // 如果 effect 配置了调度器,优先使用调度器(computed/watch/组件更新等都依赖此机制实现自定义调度策略)
      effect.scheduler()
    } else {
      // 没有调度器则直接执行 effect 的 run 方法
      effect.run()
    }
  })
}

为什么要复制一份 effects?

直接遍历 dep 执行 effect 时,effect 执行可能导致 dep 集合发生变化(添加或删除),导致无限循环。复制一份可以避免这个问题。

scheduler 的作用

如果 effect 有 scheduler,就调用 scheduler 而不是直接执行 run。这是后续 computedwatch、组件更新等高级功能的基础 —— 它们都需要自定义的调度策略。

工具函数

ts
// 使用 const enum 定义响应式标识符,编译时会被内联替换为字符串常量,零运行时开销
export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive', // 标识一个对象是否为 reactive 代理
  RAW = '__v_raw', // 用于获取 reactive 代理背后的原始对象
}

// 判断一个值是否为响应式对象:通过访问特殊标识属性触发 proxy 的 get 拦截器
export function isReactive(value: unknown): boolean {
  return !!(value && (value as any)[ReactiveFlags.IS_REACTIVE])
}

// 获取响应式对象的原始值:通过访问 __v_raw 触发 get 拦截器返回原始对象
// 递归调用 toRaw 是为了处理多层代理嵌套的情况
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as any)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

测试用例

ts
describe('reactive', () => {
  // 测试 reactive 是否正确创建了响应式代理对象
  it('should return reactive object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    // 代理对象应该是一个新对象,而非原始对象本身
    expect(observed).not.toBe(original)
    // 代理对象应该能被 isReactive 正确识别为响应式对象
    expect(isReactive(observed)).toBe(true)
    // 原始对象不应该被误判为响应式对象
    expect(isReactive(original)).toBe(false)
    // 代理对象应该能正确读取到原始对象的属性值
    expect(observed.foo).toBe(1)
  })

  // 测试嵌套对象是否也被惰性地转换为响应式
  it('should make nested values reactive', () => {
    const original = { nested: { foo: 1 } }
    const observed = reactive(original)
    // 访问嵌套属性时,返回值也应该是响应式的(惰性转换的体现)
    expect(isReactive(observed.nested)).toBe(true)
  })

  // 测试同一对象多次调用 reactive 是否返回同一个代理(缓存机制)
  it('should return same proxy for same object', () => {
    const original = { foo: 1 }
    // 两次调用应该返回完全相同的 proxy 实例
    expect(reactive(original)).toBe(reactive(original))
  })
})

本节小结

  1. 三层 Map 结构targetMap → depsMap → dep,精确追踪每个属性的依赖
  2. Proxy 拦截 — get 时 track 收集依赖,set 时 trigger 触发更新
  3. 惰性递归 — 只在访问时才递归转换,优于 Vue 2 的初始化遍历
  4. Reflect API — 正确处理 receiver,确保 this 指向

下一节我们实现 effect,让整个响应式链路跑通。

用心学习,用代码说话 💻