四、副作用清理:避免过期副作用
当侦听器的回调包含异步操作(如请求、定时器)时,若数据源在异步操作完成前变化,可能导致“过期的副作用”(如旧请求的结果覆盖新请求)。此时需通过“清理函数”取消过期操作。
Vue 提供两种注册清理函数的方式:
1. 通过 onCleanup参数(全版本支持)
watch 的回调和 watchEffect 的回调都可接收一个 onCleanup 函数作为参数,调用它注册清理逻辑——当侦听器“即将重新执行”或“被停止”时,会先执行清理函数:
// 用 watch 示例:取消过期请求
watch(id, (newId, oldId, onCleanup) => {
// 创建 AbortController 用于取消请求
const controller = new AbortController()
// 发起请求(用 signal 关联 controller)
fetch(`/api/${newId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => {
// 仅当请求成功且未被取消时,更新数据
result.value = data
})
// 注册清理函数:当 id 变化(侦听器即将重新执行)时,取消当前请求
onCleanup(() => {
controller.abort() // 终止请求
})
})
// 用 watchEffect 示例:清理定时器
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('计数:', count.value++)
}, 1000)
// 注册清理函数:当依赖变化或侦听器停止时,清除定时器
onCleanup(() => {
clearInterval(timer)
})
})作用:确保过期的异步操作(如旧请求、旧定时器)被及时终止,避免干扰新的副作用。
2. 通过 onWatcherCleanupAPI(Vue 3.5+ 支持)
Vue 3.5+ 新增 onWatcherCleanup 函数,可在回调中直接调用注册清理逻辑(需在同步阶段调用):
import { watch, onWatcherCleanup } from 'vue'
watch(id, async (newId) => {
const controller = new AbortController()
// 同步阶段调用 onWatcherCleanup 注册清理函数
onWatcherCleanup(() => {
controller.abort()
})
const res = await fetch(`/api/${newId}`, { signal: controller.signal })
// ...
})五、回调的触发时机(flush 选项)
当响应式状态变化时,Vue 会批量处理组件更新和侦听器回调(避免频繁触发)。flush 选项用于控制“侦听器回调的执行时机”,有 3 种取值:
flush 取值 | 触发时机 | 适用场景 |
'pre' | 默认值。在组件更新前执行 | 需在 DOM 更新前读取旧 DOM 状态时 |
'post' | 在组件更新后执行(DOM 已更新) | 需在 DOM 更新后操作新 DOM(如获取新尺寸) |
'sync' | 同步执行(状态变化时立即触发,不批量处理) | 需立即响应状态变化(谨慎使用,可能影响性能) |
示例:flush: 'post' 获取更新后的 DOM
const count = ref(0)
const domHeight = ref(0)
// 组件更新后执行:确保能获取到最新的 DOM 高度
watchEffect(() => {
// 当 count 变化时,组件重新渲染(DOM 更新),之后执行回调
domHeight.value = document.getElementById('box').offsetHeight
}, { flush: 'post' })
// 更简洁的写法:watchPostEffect(`flush: 'post'` 的别名)
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
domHeight.value = document.getElementById('box').offsetHeight
})同步侦听器注意点: flush: 'sync'(或别名 watchSyncEffect)会在状态变化时立即触发回调,不进行批量处理。若用于频繁变化的数据源(如数组批量 push 元素),可能导致回调多次执行,建议仅在必要时使用(如简单布尔值的状态监控)。
六、停止侦听器
多数情况下,我们无需手动停止侦听器:在 <script setup> 中同步创建的侦听器(watch 或 watchEffect)会自动绑定到组件实例,当组件卸载时自动停止,避免内存泄漏。
但以下情况需手动停止:
- 侦听器是异步创建的(如在 setTimeout、Promise.then 中创建)——不会绑定到组件,需手动停止;
- 需在组件未卸载时,主动取消侦听(如满足某个条件后不再需要响应状态变化)。
手动停止方法:watch 和 watchEffect 都会返回一个“停止函数”,调用该函数即可停止侦听器:
// 创建侦听器,获取停止函数
const unwatch = watchEffect(() => {
console.log(count.value)
})
// 需停止时调用(如按钮点击后)
const stopListener = () => {
unwatch() // 调用后,侦听器不再响应 count 的变化
}
// 异步创建的侦听器:必须手动停止
setTimeout(() => {
const asyncUnwatch = watchEffect(() => {
console.log('异步侦听器')
})
// 合适的时机调用停止(如组件卸载前)
onBeforeUnmount(() => {
asyncUnwatch()
})
}, 1000)总结
- watch 适合“精准控制数据源”“需要新旧值”“依赖明确”的场景,需手动指定侦听目标;
- watchEffect 适合“依赖多且动态”“无需旧值”“追求简洁”的场景,自动追踪回调中的响应式依赖;
- 副作用清理(onCleanup)可解决异步操作的过期问题,确保副作用的准确性;
- 回调时机(flush 选项)需根据 DOM 操作需求选择,优先使用默认的批量处理提升性能;
- 同步创建的侦听器自动绑定组件,异步创建的需手动停止,避免内存泄漏。
根据实际场景选择合适的侦听器,既能保证逻辑清晰,也能避免不必要的性能开销。
