4、前置知识:ES6 的 Set、Map、WeakMap、WeakSet 在响应式中的应用

说实话,很多同学学 Vue 源码时,卡就卡在「看不懂为什么用 WeakMap 而不用 Map」这种细节上。我个人觉得,搞懂这几个 ES6 集合类型,是理解 Vue3 响应式系统的第一道门槛。

今天咱们就把它们掰开揉碎,看看 Vue 到底是怎么用它们的。

4.1 Set:不重复的「依赖收集器」

Set 的特点很简单——值唯一,不会重复。这在响应式里太有用了。

你想想看,一个响应式对象的属性,可能被多个 effect(副作用函数)依赖。如果同一个 effect 被重复添加,那执行时就会重复触发,造成性能浪费。

核心场景:Vue3 中用 Set 来存储每个属性的依赖集合。

// 伪代码:每个属性对应一个 Set
const dep = new Set()

function track(target, key) {
  if (activeEffect) {
    dep.add(activeEffect)  // 自动去重
  }
}

function trigger(target, key) {
  dep.forEach(effect => effect())  // 遍历执行
}

我在项目中遇到过一个问题:某个组件渲染了两次,排查半天发现是依赖被重复添加了。用 Set 之后,这种问题就再也没出现过。

小技巧:Set 的 forEach 遍历顺序就是插入顺序,这在调试时很友好。

4.2 Map:对象到依赖的「映射表」

Map 允许用对象作为 key。这在响应式里是刚需——我们需要把「一个响应式对象」映射到「它的所有属性依赖」。

// Vue3 核心数据结构
const targetMap = new Map()

function track(target, key) {
  // 1. 从 targetMap 中取出该对象对应的 depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  // 2. 从 depsMap 中取出该属性对应的 dep
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  // 3. 收集依赖
  dep.add(activeEffect)
}

结构是这样的:

层级 类型 作用
targetMap WeakMap 对象 → depsMap
depsMap Map 属性名 → dep
dep Set effect 集合

说白了,这就是一个三层嵌套的查找结构。我刚开始看源码时,也被这个嵌套绕晕过。后来自己画了个图,一下就明白了。

4.3 WeakMap:内存泄漏的「终结者」

WeakMap 和 Map 最大的区别:key 是弱引用

什么意思?如果一个对象只被 WeakMap 引用,没有其他地方引用它,那它就会被垃圾回收掉。

注意:Vue3 的 targetMap 用的是 WeakMap,而不是 Map。为什么?

假设你有一个组件,它创建了一个响应式对象。组件销毁后,这个对象应该被回收。但如果 targetMap 是 Map,它会强引用这个对象,导致内存泄漏。

我曾经在一个老项目中用过 Map 做缓存,结果页面切换几十次后,内存直接飙到 200MB。换成 WeakMap 后,内存就稳定了。

// 正确做法:Vue3 用的 WeakMap
const targetMap = new WeakMap()

// 错误做法:如果用 Map,对象永远不会被回收
// const targetMap = new Map()  // ❌ 内存泄漏

一句话总结:WeakMap 让「不再需要的响应式对象」自动被垃圾回收,不用手动清理。

4.4 WeakSet:标记已处理的对象

WeakSet 和 WeakMap 类似,key 也是弱引用。Vue3 用它来标记「已经被代理过的对象」。

// Vue3 源码中的实际用法
const reactiveMap = new WeakMap()

function reactive(target) {
  // 如果已经代理过,直接返回
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  // 创建代理并缓存
  const proxy = new Proxy(target, handler)
  reactiveMap.set(target, proxy)
  return proxy
}

嗯,这里要注意:WeakSet 和 WeakMap 都不能遍历。你不能用 forEach 或 size 属性。所以它只适合做「标记」或「缓存」这类场景。

4.5 四种集合的对比总结

类型 key 类型 引用方式 可遍历 Vue 中的用途
Set 任意值 强引用 存储属性的依赖集合(effect)
Map 任意值 强引用 存储属性名到 dep 的映射
WeakMap 对象 弱引用 存储对象到 depsMap 的映射
WeakSet 对象 弱引用 标记已代理的对象

我的建议:如果你在写自己的响应式库,记住这个原则——

  • 需要遍历的用 Set/Map
  • 不需要遍历、且 key 是对象的用 WeakMap/WeakSet

4.6 手写一个迷你响应式系统

光说不练假把式。咱们用上面这些知识,写一个最简单的响应式系统:

// 全局依赖存储
const targetMap = new WeakMap()
let activeEffect = null

function effect(fn) {
  activeEffect = fn
  fn()  // 执行时触发 track
  activeEffect = null
}

function track(target, key) {
  if (!activeEffect) return
  
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

// 测试
const data = { count: 0 }
const proxy = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
    trigger(target, key)
    return true
  }
})

effect(() => {
  console.log('count 变了:', proxy.count)
})

proxy.count = 1  // 输出: count 变了: 1
proxy.count = 2  // 输出: count 变了: 2

你看,核心就几十行代码。Set、Map、WeakMap 各司其职,配合 Proxy 就实现了响应式。

最后说一句:Vue3 的响应式比这个复杂得多,但底层的数据结构思路是一样的。搞懂了这四个集合类型,你就已经站在了理解 Vue3 响应式的起跑线上。

下一节,咱们会深入 Proxy 和 Reflect,看看 Vue3 是怎么用它们实现「全量拦截」的。