4.3 设计一个完善的响应系统
把「桶」从单层
Set重构为WeakMap → Map → Set三层结构,让副作用函数只与真正读过的字段建立联系。
一、响应系统的工作流程
- 读取操作发生 → 把副作用函数收集到「桶」中;
- 设置操作发生 → 从「桶」中取出副作用函数并执行。
上一节已经用 activeEffect + effect(fn) 让匿名副作用函数也能被收集,但还剩一个错误行为:
effect(() => {
console.log('effect run') // 期望只打印 1 次
document.body.innerText = obj.text
})
setTimeout(() => {
// 副作用函数中并没有读取 notExist 属性
obj.notExist = 'hello vue3' // 却触发了副作用函数重新执行 → 不正确
}, 1000)
原因:桶是单层 Set,只记录了「有哪些副作用函数」,没记录「它们读过哪个 target 的哪个 key」。
obj.notExist与副作用函数之间其实没有响应联系。
二、重新设计桶的结构
WeakMap ── target ──→ Map ── key ──→ Set ──→ effects
| 层级 | 类型 | 作用 |
|---|---|---|
| 第一层 | WeakMap | 由 target 找到它的 Map |
| 第二层 | Map | 由 key 找到依赖它的副作用函数集合 |
| 第三层 | Set | 存放所有依赖该 key 的副作用函数,自动去重 |
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
if (!activeEffect) return target[key]
// 1. 取出 target 对应的 Map
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 2. 取出 key 对应的 Set
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 3. 收集当前副作用函数
deps.add(activeEffect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
})
最后一层用
Set:同一个副作用函数多次读取同一个key时不会重复添加。
为什么第一层用 WeakMap
WeakMap对 key 是弱引用,不影响垃圾回收器的工作:一旦target被回收,对应的键值自动消失;- 若换成
Map,即使外界已不再引用target,它也会永远留在桶里无法回收,可能导致内存泄漏。
三、封装成 track 与 trigger
const obj = new Proxy(data, {
get(target, key) {
track(target, key) // 追踪:收集依赖
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key) // 触发:派发更新
}
})
// 追踪变化
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
// 触发变化
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
封装的意义:
track只管收集、trigger只管触发。后续要加cleanup、调度器、守卫条件时都只改这两个函数,Proxy拦截器一行都不用动。
四、易错点
get拦截里不要漏掉if (!activeEffect) return target[key],否则在非副作用函数上下文读取数据时会收集到undefined。trigger中depsMap不存在时直接return:说明这个target从未被读取过,不需要任何响应。
相关
- 4.2 响应式数据的基本实现:单层 Set 桶的最小实现与它的两处硬伤
- 4.4 分支切换与 cleanup:三层桶带来的新问题——依赖会「遗留」