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 是怎么用它们实现「全量拦截」的。