Vue3 的响应式系统源码级 watchEffect 的立即执行与副作用清理机制
字数 2719
更新时间 2026-01-01 06:38:26

Vue3 的响应式系统源码级 watchEffect 的立即执行与副作用清理机制


一、题目描述

本题旨在深入剖析 Vue3 的 watchEffect API 的两个核心行为特性:立即执行副作用清理 的底层实现原理。我们将从源码角度,分析 watchEffect 如何在其创建时立即运行其副作用函数,以及如何在每次副作用函数重新执行前或 watchEffect 停止时,自动执行用户定义的清理函数,从而有效避免内存泄漏和逻辑冲突。

二、解题过程

1. 基本定义与入口

watchEffect 是一个用于立即执行一个副作用函数,并自动追踪其依赖的响应式数据,当依赖发生变化时自动重新运行该函数的高级 API。其类型定义为:

function watchEffect(
  effect: (onCleanup: OnCleanup) => void,
  options?: WatchEffectOptions
): StopHandle

其中,effect 是副作用函数,它接收一个 onCleanup 函数作为参数,用于注册清理回调。options 可包含 flush 等选项,但本题聚焦于“立即执行”和“副作用清理”机制。

在源码中,watchEffectdoWatch 函数的一个特化封装。

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 对应的 cbnull,所以会进入 else 分支。
  • effect.run() 会立即执行副作用函数 getter(对于 watchEffectgetter 就是用户传入的 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
}

流程解析:

  1. onCleanup 是一个闭包函数,它接受用户提供的清理函数 fn
  2. 当用户在其副作用函数中调用 onCleanup(fn) 时,实际上调用的是这个闭包函数。
  3. 这个闭包函数将用户传入的 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. 执行流程与清理时机总结

  1. 创建与立即执行:调用 watchEffect(fn) 时,创建 ReactiveEffect 实例,并立即同步执行 effect.run() -> getter() -> 用户副作用函数 fn。在 fn 执行过程中,访问到的响应式属性会被追踪为依赖。用户可以在这时调用 onCleanup 注册清理函数。
  2. 依赖变化,重新执行
    • 当任何被追踪的依赖发生变化时,会触发 effect 的调度(scheduler),最终安排 job() 的执行。
    • job() 会调用 effect.run() -> getter()
    • getter() 内部,首先执行上一次注册的清理函数(如果有),然后再次运行用户副作用函数 fn,并允许其注册新的清理函数。
  3. 停止监听:调用 watchEffect 返回的停止函数,会执行 effect.stop(),该方法也会触发当前注册的清理函数,然后标记 effect 为未激活状态,使其不再响应依赖变化。

5. 核心优势与设计思想

  • 立即执行:确保了副作用函数在组件挂载后(或 watchEffect 创建后)能立即执行一次,以初始化状态或执行首次操作。
  • 自动清理:通过将清理函数的调用与副作用执行周期、effect 生命周期绑定,为开发者提供了声明式资源管理的能力。开发者只需关注“每次执行时要清理什么”,而无需手动管理清理时机,极大地减少了因忘记清理而导致的内存泄漏和逻辑错误。

这个机制是 watchEffect 相较于手动在 onMounted/onUpdated 中管理副作用,以及相较于 watch(其清理函数仅在回调执行前调用,且不随 immediate:true 而立即执行)的一大优势。

相似文章
相似文章
 全屏