2025.09.22

watch:副作用清理

📝 简介

Vue 的组合式 API 中,watch 函数不仅能响应式地跟踪数据的变化,还提供了一个关键机制来管理“副作用清理”(Side Effect Cleanup)。这是避免内存泄漏和行为混乱的重要手段。

🧐 为什么需要副作用清理?

在某些场景下,当响应式数据发生变化时,我们执行的操作(副作用)可能会在下一次变化发生时变得过时或无效。如果不手动清理这些过时的副作用,它们就会继续存在于内存中,导致以下问题:

  1. 内存泄漏: 例如,旧的定时器、旧的网络请求或旧的事件监听器没有被移除。
  2. 行为错误: 多个过时的事件监听器或逻辑同时响应,导致应用行为混乱。
const flag = ref(true)
watch(flag, (newValue, oldValue, onCleanup) => {
  const dom = newValue ? app : div // 依赖于 flag 决定给哪个 DOM 元素添加事件
  const handler = () => {
    console.log(newValue ? '点击了 app' : '点击了 div')
  }

  dom.addEventListener('click', handler)

  // 关键:注册清理函数
  onCleanup(() => {
    dom.removeEventListener('click', handler)
  })
}, {
  immediate: true
})

🚫 未使用 onCleanup 的问题:

  1. 初始状态 (flag: true): 在 #app 上添加了 click 监听。
  2. 点击按钮(flag -> false)
    • 新的监听器在 #div 上添加
    • 旧的监听器在 #app 上没有被移出
  3. 结果: 此时点击 #app#div 都会触发相应的 console.log, 但是从逻辑上来讲, 当 flagfalse时, #app 上的监听器应该失效了

✅ 使用 onCleanup 的解决方案: Vue 提供的 onCleanup 回调函数,确保在下一次副作用运行之前,上一次的副作用会被自动撤销,完美解决了事件残留的问题。

🛠️ 实现原理

步骤动作描述
1. 注册清理onCleanup将提供的清理函数 cb 赋值给内部变量 cleanup
2. 准备执行job响应式依赖变化 准备执行新的副作用
3. 执行清理if(cleanup){cleanup();cleanup = nul}检查并执行上次注册的 onCleanup函数,然后将其置空
4. 执行副作用cb(newValue, oldValue, onCleanup)执行本次的副作用逻辑, 并在内部重新注册新的cleanup函数
export function watch(source, cb, options: WatchOptions = EMPTY_OBJ) {
  // 存储清理函数
  let cleanup: (() => void) | null = null

  function onCleanup(fn: () => void) {
    cleanup = fn
  }

  function job() {
    // ⬇️ 步骤 3: 清理上一次的副作用
    if (cleanup) {
      cleanup() // 执行移除监听、取消请求等操作
      cleanup = null
    }

    // ⬇️ 步骤 4: 执行本次的副作用(在回调中可以重新调用 onCleanup 注册新的清理函数)
    const newValue = effect.run()
    cb(newValue, oldValue, onCleanup)

    oldValue = newValue
  }

  // ... 其他逻辑
}

💡 使用 onCleanup,可以确保当 watch 的依赖源变化时,应用始终处于一个干净且最新的状态。