D06 · effect / track / trigger 三件套
对应主课: L31-L32 响应式原理 + 依赖追踪 最后核对: 2026-09-23
1. 三者的关系
2. 数据结构
下面是教学用的 Set 模型,便于观察订阅关系;Vue 3.5.43 使用 Dep、Subscriber、带版本号的双向 Link 管理连接,并不逐字等于这个结构。
targetMap (WeakMap)
└─ target 对象 → depsMap (Map)
├─ 'count' → deps (Set) → { effectA, effectB }
└─ 'name' → deps (Set) → { effectC }- WeakMap:映射本身不让 target 因为充当键而一直存活;其他闭包或对象仍可能持有 target,GC 也不能代替 stop 与外部资源清理
- Map:每个属性一个依赖集合
- Set:同一个 effect 不会重复收集
3. track 实现
完整代码在 §7。track 的关键是同时去重两边的连接:
// dep 是当前 target/key 的 Set;activeEffect 是正在收集依赖的实例。
if (activeEffect?.active && !dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}只在 Set 里去重还不够:若每次读同一属性都 push,effect.deps 会重复保存同一个集合。反向列表用于 cleanup,在重跑或 stop 时把 effect 从旧集合移走。
4. trigger 实现
触发时遍历依赖快照,避免 effect 在 cleanup 后又重新加入正在遍历的 Set。只处理仍活动、且当前不在执行栈中的 effect;有 scheduler 则传入同一个稳定 runner,由调度器决定何时执行:
for (const item of new Set(dep)) {
if (!item.active || item.running) continue
if (item.scheduler) item.scheduler(item.runner)
else item.run()
}这段是 §7 的局部逻辑。§7 默认同步执行,不把 effect 等同于组件更新的异步 job;接入 L35 的队列才会批处理。
为什么跳过正在执行的 effect
如果 effect 内既读又写同一个 count,写入可能立即再调用自己。running guard 可以阻止当前调用栈里的自重入,也能保护仍在运行的外层 effect。不过它不保证消除多个 effect 或异步回调形成的所有反馈循环。业务上仍应避免在派生计算或渲染中无条件修改自己的来源。
5. effect 的 cleanup
这是“全清后重收”的教学算法。真实 Vue 3.5.43 会在运行前准备依赖版本,再清理本次未使用的 Link;run 前后还要恢复活动订阅者,不能让嵌套 effect 或异常破坏外层上下文。固定版本 effect.ts
6. 在 Vue 组件中的应用
| 场景 | 内部机制 |
|---|---|
模板绑定 | render effect 中读取 → track |
computed(() => ...) | 3.5 的 ComputedRefImpl 自身是 Subscriber,按依赖/全局版本惰性刷新;L32 的 effect+dirty 是教学模型 |
watch(source, cb) | effect 读取来源;job 检查变化与清理后调用 cb,受 flush 等选项影响 |
watchEffect(() => ...) | 在同步执行段追踪依赖;首次与后续运行时序受 flush 影响 |
7. 动手实验:可清理、可停止的教学模型
在现代浏览器控制台一次性运行。这个模型只处理普通对象的浅层数据属性,不实现嵌套转换、枚举、数组/集合、computed 或组件。L32 继续展示嵌套响应式、computed 与 ref 的组合:
function createMiniReactivity() {
const targetMap = new WeakMap()
const cache = new WeakMap()
const proxies = new WeakSet()
let activeEffect
function cleanup(item) {
for (const dep of item.deps) dep.delete(item)
item.deps.length = 0
}
class ReactiveEffect {
deps = []
active = true
running = false
constructor(fn, scheduler) {
this.fn = fn
this.scheduler = scheduler
this.runner = this.run.bind(this)
this.runner.effect = this
}
run() {
if (!this.active) return this.fn()
if (this.running) return
cleanup(this)
const previous = activeEffect
activeEffect = this
this.running = true
try { return this.fn() }
finally { activeEffect = previous; this.running = false }
}
stop() {
this.active = false
cleanup(this)
}
}
function track(target, key) {
if (!activeEffect?.active) 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)
}
}
function trigger(target, key) {
const dep = targetMap.get(target)?.get(key)
if (!dep) return
for (const item of new Set(dep)) {
if (!item.active || item.running) continue
if (item.scheduler) item.scheduler(item.runner)
else item.run()
}
}
function reactive(target) {
if (target === null || typeof target !== 'object') throw new Error('需要普通对象')
const proto = Object.getPrototypeOf(target)
if (proto !== Object.prototype && proto !== null) throw new Error('只支持普通对象')
if (proxies.has(target)) return target
if (cache.has(target)) return cache.get(target)
const proxy = new Proxy(target, {
get(object, key, receiver) {
const value = Reflect.get(object, key, receiver)
track(object, key)
return value
},
set(object, key, value, receiver) {
const had = Object.prototype.hasOwnProperty.call(object, key)
const old = Reflect.get(object, key, receiver)
const result = Reflect.set(object, key, value, receiver)
if (result && (!had || !Object.is(old, value))) trigger(object, key)
return result
},
deleteProperty(object, key) {
const had = Object.prototype.hasOwnProperty.call(object, key)
const result = Reflect.deleteProperty(object, key)
if (result && had) trigger(object, key)
return result
},
})
cache.set(target, proxy)
proxies.add(proxy)
return proxy
}
function effect(fn, { scheduler, lazy = false } = {}) {
const item = new ReactiveEffect(fn, scheduler)
if (!lazy) {
try { item.run() } catch (error) { item.stop(); throw error }
}
return item.runner
}
function stop(runner) { runner.effect.stop() }
return { reactive, effect, stop }
}
const { reactive, effect, stop } = createMiniReactivity()
const state = reactive({ showA: true, a: 1, b: 10, count: 0 })
let runs = 0
let selected
const branch = effect(() => {
runs++
selected = state.showA ? state.a : state.b
console.log('选中值:', selected)
})
state.a = 2
state.showA = false
state.a = 3 // 已不再订阅 a
console.assert(runs === 3 && selected === 10)
state.b = 11
stop(branch)
state.b = 12
console.assert(runs === 4 && selected === 11)
branch() // 已停止 runner 可以手动执行,但不重新成为订阅者
state.b = 13
console.assert(runs === 5 && selected === 12)
const jobs = new Set()
let seen = -1
const queued = effect(() => { seen = state.count }, { scheduler: job => jobs.add(job) })
state.count = 1
state.count = 2
console.assert(jobs.size === 1 && seen === 0)
for (const job of jobs) if (job.effect.active) job()
jobs.clear()
console.assert(seen === 2)
stop(queued)这里的 scheduler 参数是稳定 runner,而不是每次触发新建的函数,也不是 ReactiveEffect 实例本身。队列调用前检查 active,避免把已经停止的 runner 当成一次手动执行。run 的 try/finally 保证异常后恢复上下文;当前同步模型会把异常交给调用者,不实现 Vue 的组件错误处理。
ref 并不是 reactive({ value: T }) 的语法简写:Vue 的 RefImpl 用 .value getter/setter,普通对象值才进一步转成响应式代理。3.5 computed 的实现也不同于此 mini;对应源码见 ref.ts、computed.ts。
8. 总结
- 本篇 effect 默认立即执行并返回 runner;lazy 是教学模型的选项,不应据此推断所有 Vue 高层 API 的首次时序
track在 getter 中收集"谁在用这个数据"trigger在 setter 中通知"数据变了,重新执行"- cleanup 解决条件分支的依赖残留
- scheduler 接管执行时机,配合稳定 runner 去重;是否异步由具体调度器决定