2025.09.22
watch:副作用清理
📝 简介
在 Vue 的组合式 API 中,watch 函数不仅能响应式地跟踪数据的变化,还提供了一个关键机制来管理“副作用清理”(Side Effect Cleanup)。这是避免内存泄漏和行为混乱的重要手段。
🧐 为什么需要副作用清理?
在某些场景下,当响应式数据发生变化时,我们执行的操作(副作用)可能会在下一次变化发生时变得过时或无效。如果不手动清理这些过时的副作用,它们就会继续存在于内存中,导致以下问题:
- 内存泄漏: 例如,旧的定时器、旧的网络请求或旧的事件监听器没有被移除。
- 行为错误: 多个过时的事件监听器或逻辑同时响应,导致应用行为混乱。
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 的问题:
- 初始状态 (
flag: true): 在#app上添加了click监听。 - 点击按钮(
flag -> false)- 新的监听器在
#div上添加 - 旧的监听器在
#app上没有被移出
- 新的监听器在
- 结果: 此时点击
#app和#div都会触发相应的console.log, 但是从逻辑上来讲, 当flag为false时,#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的依赖源变化时,应用始终处于一个干净且最新的状态。