Vue 侦听器(watch 与 watchEffect)全解析3

四、副作用清理:避免过期副作用

当侦听器的回调包含异步操作(如请求、定时器)时,若数据源在异步操作完成前变化,可能导致“过期的副作用”(如旧请求的结果覆盖新请求)。此时需通过“清理函数”取消过期操作。

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>同步创建的侦听器(watchwatchEffect)会自动绑定到组件实例,当组件卸载时自动停止,避免内存泄漏。

但以下情况需手动停止:

  • 侦听器是异步创建的(如在 setTimeoutPromise.then 中创建)——不会绑定到组件,需手动停止;
  • 需在组件未卸载时,主动取消侦听(如满足某个条件后不再需要响应状态变化)。

手动停止方法watchwatchEffect 都会返回一个“停止函数”,调用该函数即可停止侦听器:

 // 创建侦听器,获取停止函数
 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 操作需求选择,优先使用默认的批量处理提升性能;
  • 同步创建的侦听器自动绑定组件,异步创建的需手动停止,避免内存泄漏。

根据实际场景选择合适的侦听器,既能保证逻辑清晰,也能避免不必要的性能开销。

原文链接:,转发请注明来源!