Appearance
第 5 章 @vue/reactivity 源码深度剖析(下):effect / effectScope / shallowReactive / readonly
本章要点
- effect() 的完整生命周期:创建、执行、依赖收集、清理、销毁
- effectScope:为什么需要"作用域"来管理副作用
- shallowReactive / shallowRef:何时需要"浅层"响应式
- readonly / shallowReadonly:编译器如何利用只读优化
- 响应式工具函数全景:toRaw / markRaw / isRef / unref / toRefs
你是否遇到过这样的场景:一个页面打开了 WebSocket 连接、启动了定时器、注册了 watchEffect——当用户离开页面时,这些副作用都需要被清理。忘记清理任何一个,都会导致内存泄漏。
在 Vue 2 中,你需要在 beforeUnmount 中手动管理每一个副作用的清理。在 Vue 3 中,effectScope 让你可以一行代码批量清理所有副作用。
但 effectScope 是如何做到的?它和 effect 之间的关系是什么?effect 本身的生命周期又是怎样的?
本章将继续深入 @vue/reactivity,完成响应式系统的最后几块拼图。
5.1 effect():响应式系统的执行引擎
effect 的本质
effect 是响应式系统中"副作用"的载体。当你写 watchEffect、watch,或者 Vue 内部创建组件的渲染更新函数时,底层都是 effect。
typescript
import { ref, effect } from '@vue/reactivity'
const count = ref(0)
// 创建一个 effect
const runner = effect(() => {
console.log(`count is: ${count.value}`)
})
// 输出: count is: 0
count.value = 1
// 输出: count is: 1(自动重新执行)
count.value = 2
// 输出: count is: 2(再次自动重新执行)ReactiveEffect 类
typescript
// packages/reactivity/src/effect.ts(简化)
export class ReactiveEffect<T = any> implements Subscriber {
// --- Subscriber 接口 ---
_deps: Link | undefined = undefined
_depsTail: Link | undefined = undefined
_flags: number = SubscriberFlags.ACTIVE
// --- Effect 特有 ---
_fn: () => T
_scheduler: EffectScheduler | undefined
_cleanup: (() => void) | undefined
// 当前作用域
_scope: EffectScope | undefined
constructor(fn: () => T) {
this._fn = fn
// 自动注册到当前活跃的 EffectScope
if (activeEffectScope) {
this._scope = activeEffectScope
activeEffectScope.effects.push(this)
}
}
run(): T {
// 1. 如果已停止,直接执行函数(不收集依赖)
if (!(this._flags & SubscriberFlags.ACTIVE)) {
return this._fn()
}
// 2. 设置当前活跃 subscriber
const prevSub = activeSub
activeSub = this
// 3. 开启追踪
const prevShouldTrack = shouldTrack
shouldTrack = true
try {
// 4. 准备清理旧依赖
prepareDeps(this)
// 5. 执行函数 — 触发 getter → track → 建立新依赖
const result = this._fn()
// 6. 清理不再需要的旧依赖
cleanupDeps(this)
return result
} finally {
// 7. 恢复之前的上下文
activeSub = prevSub
shouldTrack = prevShouldTrack
}
}
stop(): void {
if (this._flags & SubscriberFlags.ACTIVE) {
// 清理所有依赖链接
removeDeps(this)
// 执行清理回调
if (this._cleanup) {
this._cleanup()
}
this._flags &= ~SubscriberFlags.ACTIVE
}
}
}依赖的生命周期管理
每次 effect.run() 执行时,需要处理一个微妙的问题:依赖可能在两次执行之间发生变化。
考虑这个场景:
typescript
const show = ref(true)
const a = ref('hello')
const b = ref('world')
effect(() => {
if (show.value) {
console.log(a.value) // 第一次执行:依赖 show 和 a
} else {
console.log(b.value) // 第二次执行:依赖 show 和 b
}
})当 show.value 从 true 变为 false 时,effect 重新执行。此时它不再依赖 a,而是依赖 b。旧的对 a 的依赖必须被清理,否则 a.value 变化时仍会触发这个 effect——这就是"过期依赖"问题。