1.Vue 3 中,watchEffect的onInvalidate是一个用于清理副作用的关键机制。它允许你在副作用函数重新执行之前,或者在监听器停止时,执行清理操作(如清除定时器、取消网络请求、解绑事件监听等),从而有效防止内存泄漏和逻辑错误。
1. 核心作用与触发时机
onInvalidate接收一个回调函数作为参数,该回调会在以下两种情况下被调用:
- 副作用即将重新执行前:当
watchEffect追踪的响应式依赖发生变化,导致副作用函数需要再次运行时,Vue 会先调用onInvalidate中的清理函数,然后再执行新的副作用逻辑。 - 监听器停止时当组件卸载或手动调用
stop()函数停止监听时。
注意:在watchEffect首次执行时不会触发onInvalidate,因为此时没有“上一次”的副作用需要清理。
2. 典型应用场景
A. 清理定时器(防止堆积)
如果在watchEffect中使用了setInterval或setTimeout,每次依赖变化都会创建一个新的定时器。如果不清理,旧的定时器会继续运行,导致多个定时器同时存在,引发性能问题。
import { ref, watchEffect } from 'vue' const count = ref(0) watchEffect((onInvalidate) => { // 1. 启动定时器 const timer = setInterval(() => { console.log('Timer running...', count.value) }, 1000) // 2. 注册清理函数 // 当下次 count 变化或组件卸载时,清除当前的 timer onInvalidate(() => { clearInterval(timer) console.log('Timer cleared') }) })B. 取消过期的异步请求(防止竞态条件)
在搜索框等场景中,用户快速输入会导致多次发起 API 请求。由于网络延迟,后发出的请求可能比先发出的请求更早返回,导致页面显示旧数据。使用onInvalidate可以标记之前的请求为“已废弃”,忽略其返回结果。
import { ref, watchEffect } from 'vue' const keyword = ref('') const results = ref([]) watchEffect(async (onInvalidate) => { if (!keyword.value) return // 标记当前请求是否有效 let isCancelled = false // 注册清理函数:如果副作用重新执行,将上一个请求标记为取消 onInvalidate(() => { isCancelled = true }) try { const res = await fetch(`/api/search?q=${keyword.value}`) const data = await res.json() // 只有当请求未被取消时,才更新数据 if (!isCancelled) { results.value = data } } catch (error) { if (!isCancelled) { console.error(error) } } })C. 解绑事件监听
避免在组件生命周期内重复绑定同一事件,造成内存泄漏或多次触发。
watchEffect((onInvalidate) => { const handler = () => console.log('Window resized') window.addEventListener('resize', handler) // 清理:移除事件监听 onInvalidate(() => { window.removeEventListener('resize', handler) }) })3. 最佳实践与注意事项
推荐使用局部变量而非 Ref 存储资源ID:
在清理定时器或请求时,建议直接在watchEffect内部使用局部变量(如let timer)来存储资源 ID,而不是将其存储在ref中。这样利用闭包特性,onInvalidate能准确捕获当前这次执行所创建的資源,代码更简洁且不易出错。执行顺序:
当依赖变化时,执行顺序为:- 触发
onTrigger(调试用) - 执行上一次的
onInvalidate清理函数 - 执行当前的副作用函数
- 触发
与
flush选项配合:watchEffect默认在组件更新前执行(flush: 'pre')。如果你需要在 DOM 更新后执行副作用(例如获取元素尺寸),可以配置flush: 'post'。无论flush设置为何值,onInvalidate都会在下次副作用执行前被调用。watchEffect((onInvalidate) => { // ... 副作用逻辑 }, { flush: 'post' // 在 DOM 更新后执行 })不要用于获取旧值:
watchEffect无法直接获取数据的“旧值”。如果你需要对比新旧值(例如判断数据是从 A 变到 B 还是从 B 变到 A),应该使用watchAPI,而不是watchEffect。