Vue3 的响应式系统源码级 watchEffect 的立即执行与副作用清理机制
一、题目描述
本题旨在深入剖析 Vue3 的 watchEffect API 的两个核心行为特性:立即执行 和 副作用清理 的底层实现原理。我们将从源码角度,分析 watchEffect 如何在其创建时立即运行其副作用函数,以及如何在每次副作用函数重新执行前或 watchEffect 停止时,自动执行用户定义的清理函数,从而有效避免内存泄漏和逻辑冲突。
二、解题过程
1. 基本定义与入口
watchEffect 是一个用于立即执行一个副作用函数,并自动追踪其依赖的响应式数据,当依赖发生变化时自动重新运行该函数的高级 API。其类型定义为:
function watchEffect(
effect: (onCleanup: OnCleanup) => void,
options?: WatchEffectOptions
): StopHandle
其中,effect 是副作用函数,它接收一个 onCleanup 函数作为参数,用于注册清理回调。options 可包含 flush 等选项,但本题聚焦于“立即执行”和“副作用清理”机制。
在源码中,watchEffect 是 doWatch 函数的一个特化封装。
2. 立即执行机制:doWatch 中的初始调用
watchEffect 的入口在 packages/runtime-core/src/apiWatch.ts 中:
export function watchEffect(
effect: WatchEffect,
options?: WatchEffectOptions
): WatchStopHandle {
return doWatch(effect, null, options)
}
核心逻辑在 doWatch 函数中。watchEffect 调用 doWatch 时,第二个参数(监听源)是 null,表示这是一个 effect 类型的监听。
在 doWatch 函数内部,有一个关键的初始化步骤:
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect,
cb: WatchCallback | null,
options: WatchOptionsBase = EMPTY_OBJ
): WatchStopHandle {
// ... 其他变量定义
const job = () => {
if (!effect.active) { return }
if (cb) {
// watch 的逻辑
} else {
// watchEffect 的逻辑:直接重新运行 effect
effect.run()
}
}
// 关键步骤:创建副作用函数(effect)
const effect = new ReactiveEffect(getter, scheduler)
effect.onTrack = onTrack
effect.onTrigger = onTrigger
// 初始运行
if (cb) {
if (options.immediate) {
job()
} else {
// 对于普通的 watch,首次不执行
oldValue = effect.run()
}
} else {
// 对于 watchEffect,无论 options 如何,在创建后立即执行一次
effect.run()
}
// ...
}
关键点在于:
watchEffect对应的cb是null,所以会进入else分支。effect.run()会立即执行副作用函数getter(对于watchEffect,getter就是用户传入的effect函数)。- 这就是“立即执行”的根源:在创建
ReactiveEffect实例后,同步调用effect.run(),从而触发用户定义的副作用函数,并在这个过程中完成依赖的首次收集。
3. 副作用清理机制:onCleanup 函数的注册与调用
用户可以在 watchEffect 的副作用函数中,通过参数 onCleanup 注册一个清理函数。这个清理函数会在两种情况下被调用:
- 在副作用函数重新执行之前(即依赖变化导致重新运行前)。
- 在
watchEffect停止时(即调用其返回的stop函数时)。
3.1 清理函数的注册
在 doWatch 函数中,会创建一个 getter 函数。对于 watchEffect,这个 getter 是对用户副作用函数的包装:
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
const getter = () => {
// 清理之前的清理函数
if (cleanup) {
cleanup()
}
// 执行用户的副作用函数,并将 onCleanup 函数传入
callWithAsyncErrorHandling(
source, // 用户的副作用函数
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup] // 将 onCleanup 作为参数传入
)
// 执行后,cleanup 已经被用户函数重新赋值(如果用户调用了 onCleanup)
// 如果没有调用,cleanup 仍然是 undefined
}
流程解析:
onCleanup是一个闭包函数,它接受用户提供的清理函数fn。- 当用户在其副作用函数中调用
onCleanup(fn)时,实际上调用的是这个闭包函数。 - 这个闭包函数将用户传入的
fn赋值给cleanup变量,同时 也将fn赋值给effect.onStop。这是一个巧妙的“一石二鸟”设计:cleanup用于在下一次副作用执行前调用。effect.onStop用于在watchEffect停止时调用。
3.2 清理函数的调用时机
(1) 重新执行前调用
在 getter 函数的开头,有一行关键代码:if (cleanup) { cleanup() }。
这意味着,每次 watchEffect 的副作用函数即将重新运行(无论是首次还是后续依赖变化触发),都会先检查是否存在上一次注册的清理函数(cleanup),如果存在,就执行它。这确保了在运行新的副作用逻辑前,旧的、可能过时的副作用(如定时器、事件监听器、未完成的异步请求等)被正确清理。
(2) 停止时调用
doWatch 函数返回一个停止句柄(StopHandle):
const unwatch = () => {
effect.stop()
if (invalidate) {
invalidate()
}
}
return unwatch
effect.stop() 会调用 ReactiveEffect 实例的 stop 方法,该方法内部会检查并执行 effect.onStop:
class ReactiveEffect<T = any> {
// ...
stop() {
if (this.active) {
cleanupEffect(this) // 清理 effect 的依赖关系
if (this.onStop) {
this.onStop() // 执行用户注册的清理函数
}
this.active = false
}
}
}
由于之前在注册清理函数时,我们同时设置了 effect.onStop = () => { callWithErrorHandling(fn, ...) },所以当 watchEffect 被停止时,用户注册的清理函数也会被执行。这保证了即使组件卸载或监听器不再需要,相关的资源也能被释放,是防止内存泄漏的关键机制。
4. 执行流程与清理时机总结
- 创建与立即执行:调用
watchEffect(fn)时,创建ReactiveEffect实例,并立即同步执行effect.run()->getter()-> 用户副作用函数fn。在fn执行过程中,访问到的响应式属性会被追踪为依赖。用户可以在这时调用onCleanup注册清理函数。 - 依赖变化,重新执行:
- 当任何被追踪的依赖发生变化时,会触发
effect的调度(scheduler),最终安排job()的执行。 job()会调用effect.run()->getter()。- 在
getter()内部,首先执行上一次注册的清理函数(如果有),然后再次运行用户副作用函数fn,并允许其注册新的清理函数。
- 当任何被追踪的依赖发生变化时,会触发
- 停止监听:调用
watchEffect返回的停止函数,会执行effect.stop(),该方法也会触发当前注册的清理函数,然后标记effect为未激活状态,使其不再响应依赖变化。
5. 核心优势与设计思想
- 立即执行:确保了副作用函数在组件挂载后(或
watchEffect创建后)能立即执行一次,以初始化状态或执行首次操作。 - 自动清理:通过将清理函数的调用与副作用执行周期、
effect生命周期绑定,为开发者提供了声明式资源管理的能力。开发者只需关注“每次执行时要清理什么”,而无需手动管理清理时机,极大地减少了因忘记清理而导致的内存泄漏和逻辑错误。
这个机制是 watchEffect 相较于手动在 onMounted/onUpdated 中管理副作用,以及相较于 watch(其清理函数仅在回调执行前调用,且不随 immediate:true 而立即执行)的一大优势。