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