Vue3 的响应式系统源码级 WeakMap 与依赖存储结构设计原理
字数 1874 2025-12-13 08:07:34

Vue3 的响应式系统源码级 WeakMap 与依赖存储结构设计原理

一、题目描述
在 Vue3 的响应式系统中,依赖收集是核心机制之一。为了实现高效的依赖跟踪,Vue3 采用了 WeakMap 作为核心数据结构来存储目标对象(target)与其依赖映射关系。这个设计涉及以下几个关键问题:

  1. 为什么使用 WeakMap 而不是 Map?
  2. 依赖存储的整体数据结构是怎样的?
  3. 这种设计如何避免内存泄漏并提升性能?

二、解题过程

步骤1:依赖存储的基本需求分析
当一个响应式对象(如通过 reactive() 创建的对象)被访问时,需要记录是哪个副作用函数(effect)访问了它的哪个属性。这需要建立两层映射关系:

  • 第一层:目标对象 → 该对象所有属性的依赖映射
  • 第二层:属性键(key) → 访问该属性的所有副作用函数集合

步骤2:为什么选择 WeakMap 作为第一层数据结构?

// 伪代码示例结构
const targetMap = new WeakMap()  // 使用 WeakMap
targetMap.set(target, depsMap)  // target 是键,depsMap 是值

关键原因

  1. 自动内存管理:WeakMap 的键是弱引用。当目标对象(target)不再被其他代码引用时,垃圾回收器会自动回收该对象,同时 WeakMap 中的对应条目也会被自动清除。这解决了 Vue2 中使用 Object 作为键导致的内存泄漏问题。
  2. 键必须是对象:WeakMap 只接受对象作为键,这正好符合我们的需求(target 一定是对象)。
  3. 不可枚举:WeakMap 没有提供遍历方法,这保护了内部数据结构不被外部意外修改。

步骤3:依赖存储的完整数据结构设计

// 完整的依赖存储结构示意
const targetMap = new WeakMap()  // 第一层:WeakMap

// 当访问 reactiveObj.name 时
const target = reactiveObj
const key = 'name'

// 获取或创建该目标的 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
  depsMap = new Map()           // 第二层:Map
  targetMap.set(target, depsMap)
}

// 获取或创建该属性的依赖集合
let dep = depsMap.get(key)
if (!dep) {
  dep = new Set()               // 第三层:Set
  depsMap.set(key, dep)
}

// 将当前活动的副作用函数添加到集合中
dep.add(activeEffect)

数据结构层次

  • 第一层WeakMap<target, Map<key, Set<effect>>>
    • 键:目标对象(target),值:depsMap
  • 第二层Map<key, Set<effect>>
    • 键:属性名(key),值:依赖集合
  • 第三层Set<effect>
    • 存储所有访问该属性的副作用函数

步骤4:Set 作为第三层数据结构的原因

  1. 自动去重:同一个副作用函数多次收集同一个属性时,Set 会自动去重,避免重复执行。
  2. 快速查找和删除:添加、删除、查找操作的时间复杂度都是 O(1)。
  3. 有序性:虽然 Set 是集合,但在现代 JavaScript 引擎中,Set 的元素是按照插入顺序存储的,这保证了副作用函数的执行顺序。

步骤5:源码中的实际实现
让我们看 Vue3 源码中的关键代码(简化版):

// 在 reactivity/src/effect.ts 中
export type Dep = Set<ReactiveEffect>  // 依赖集合类型
export type KeyToDepMap = Map<string | symbol, Dep>  // 属性到依赖集合的映射

// 全局的依赖存储
export const targetMap = new WeakMap<any, KeyToDepMap>()

// 收集依赖的函数
export function track(target: object, type: TrackOpTypes, key: unknown) {
  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()))
  }
  
  // 如果当前副作用函数还未收集,则添加
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    // 同时,副作用函数也需要记录自己被哪些依赖集合引用
    activeEffect.deps.push(dep)
  }
}

步骤6:依赖清理的设计
当副作用函数重新执行时,需要先清理旧的依赖,再重新收集。为此,每个 effect 都维护了自己的 deps 数组:

class ReactiveEffect {
  deps: Dep[] = []  // 存储所有包含此 effect 的依赖集合
  
  // 清理函数
  cleanup() {
    for (const dep of this.deps) {
      dep.delete(this)  // 从每个依赖集合中移除自己
    }
    this.deps.length = 0  // 清空数组
  }
}

这样设计确保了:

  1. 当响应式对象的属性变化时,能快速找到所有需要重新执行的副作用函数
  2. 当副作用函数不再需要时,能正确清理所有依赖关系

步骤7:嵌套对象的处理
对于嵌套对象,Vue3 采用延迟代理(lazy proxy)策略:

const obj = reactive({
  nested: {  // 不会立即代理 nested 对象
    foo: 1
  }
})

// 只有当访问 obj.nested 时,才会创建嵌套对象的代理
console.log(obj.nested.foo)  // 此时才会创建 nested 的代理

这意味着:

  1. 外层对象的依赖存储中,nested 键对应一个依赖集合
  2. 当创建了 nested 的代理后,它会有自己的 targetMap 条目
  3. 这种设计避免了不必要的代理创建,提升了性能

步骤8:Map/Set 等集合类型的特殊处理
对于 Map、Set 等内置集合类型,Vue3 有特殊处理:

const map = reactive(new Map([['key', 'value']]))

// 对于 Map 的 size 属性,需要特殊跟踪
// 因为 size 的变化可能由多种操作引起(set、delete、clear 等)

Vue3 通过重写集合方法(如 set、delete、clear)并在这些方法中手动调用 trigger 来确保依赖正确触发。

步骤9:性能优化考虑

  1. 惰性创建:只有当属性第一次被访问时,才会创建对应的 Map 和 Set,避免初始化开销。
  2. WeakMap 的自动清理:当响应式对象不再被引用时,整个依赖树会被自动垃圾回收。
  3. 最小化依赖集合:每个属性只创建必要的 Set,避免内存浪费。

步骤10:与 Vue2 的对比
Vue2 使用的是 Object 作为依赖存储的键,这会导致:

  1. 对象作为字符串键被存储,无法被垃圾回收
  2. 需要手动管理依赖清理
  3. 对于大型应用容易造成内存泄漏

Vue3 的 WeakMap 设计从根本上解决了这些问题。

三、总结
Vue3 的依赖存储设计采用了 WeakMap → Map → Set 的三层结构,这个设计的精妙之处在于:

  1. 利用 WeakMap 的弱引用特性,实现了自动内存管理
  2. 通过 Map 建立了属性到依赖的精确映射
  3. 通过 Set 实现了依赖的去重和高效管理
  4. 整个设计既保证了依赖跟踪的准确性,又兼顾了性能和内存效率

这种数据结构设计是 Vue3 响应式系统高效、可靠的基础,也是现代前端框架中优秀的数据结构应用范例。

Vue3 的响应式系统源码级 WeakMap 与依赖存储结构设计原理 一、题目描述 在 Vue3 的响应式系统中,依赖收集是核心机制之一。为了实现高效的依赖跟踪,Vue3 采用了 WeakMap 作为核心数据结构来存储目标对象(target)与其依赖映射关系。这个设计涉及以下几个关键问题: 为什么使用 WeakMap 而不是 Map? 依赖存储的整体数据结构是怎样的? 这种设计如何避免内存泄漏并提升性能? 二、解题过程 步骤1:依赖存储的基本需求分析 当一个响应式对象(如通过 reactive() 创建的对象)被访问时,需要记录是哪个副作用函数(effect)访问了它的哪个属性。这需要建立两层映射关系: 第一层:目标对象 → 该对象所有属性的依赖映射 第二层:属性键(key) → 访问该属性的所有副作用函数集合 步骤2:为什么选择 WeakMap 作为第一层数据结构? 关键原因 : 自动内存管理 :WeakMap 的键是弱引用。当目标对象(target)不再被其他代码引用时,垃圾回收器会自动回收该对象,同时 WeakMap 中的对应条目也会被自动清除。这解决了 Vue2 中使用 Object 作为键导致的内存泄漏问题。 键必须是对象 :WeakMap 只接受对象作为键,这正好符合我们的需求(target 一定是对象)。 不可枚举 :WeakMap 没有提供遍历方法,这保护了内部数据结构不被外部意外修改。 步骤3:依赖存储的完整数据结构设计 数据结构层次 : 第一层 : WeakMap<target, Map<key, Set<effect>>> 键:目标对象(target),值:depsMap 第二层 : Map<key, Set<effect>> 键:属性名(key),值:依赖集合 第三层 : Set<effect> 存储所有访问该属性的副作用函数 步骤4:Set 作为第三层数据结构的原因 自动去重 :同一个副作用函数多次收集同一个属性时,Set 会自动去重,避免重复执行。 快速查找和删除 :添加、删除、查找操作的时间复杂度都是 O(1)。 有序性 :虽然 Set 是集合,但在现代 JavaScript 引擎中,Set 的元素是按照插入顺序存储的,这保证了副作用函数的执行顺序。 步骤5:源码中的实际实现 让我们看 Vue3 源码中的关键代码(简化版): 步骤6:依赖清理的设计 当副作用函数重新执行时,需要先清理旧的依赖,再重新收集。为此,每个 effect 都维护了自己的 deps 数组: 这样设计确保了: 当响应式对象的属性变化时,能快速找到所有需要重新执行的副作用函数 当副作用函数不再需要时,能正确清理所有依赖关系 步骤7:嵌套对象的处理 对于嵌套对象,Vue3 采用延迟代理(lazy proxy)策略: 这意味着: 外层对象的依赖存储中, nested 键对应一个依赖集合 当创建了 nested 的代理后,它会有自己的 targetMap 条目 这种设计避免了不必要的代理创建,提升了性能 步骤8:Map/Set 等集合类型的特殊处理 对于 Map、Set 等内置集合类型,Vue3 有特殊处理: Vue3 通过重写集合方法(如 set、delete、clear)并在这些方法中手动调用 trigger 来确保依赖正确触发。 步骤9:性能优化考虑 惰性创建 :只有当属性第一次被访问时,才会创建对应的 Map 和 Set,避免初始化开销。 WeakMap 的自动清理 :当响应式对象不再被引用时,整个依赖树会被自动垃圾回收。 最小化依赖集合 :每个属性只创建必要的 Set,避免内存浪费。 步骤10:与 Vue2 的对比 Vue2 使用的是 Object 作为依赖存储的键,这会导致: 对象作为字符串键被存储,无法被垃圾回收 需要手动管理依赖清理 对于大型应用容易造成内存泄漏 Vue3 的 WeakMap 设计从根本上解决了这些问题。 三、总结 Vue3 的依赖存储设计采用了 WeakMap → Map → Set 的三层结构,这个设计的精妙之处在于: 利用 WeakMap 的弱引用特性,实现了自动内存管理 通过 Map 建立了属性到依赖的精确映射 通过 Set 实现了依赖的去重和高效管理 整个设计既保证了依赖跟踪的准确性,又兼顾了性能和内存效率 这种数据结构设计是 Vue3 响应式系统高效、可靠的基础,也是现代前端框架中优秀的数据结构应用范例。