Code前端首页关于Code前端联系我们

Vue3的watch怎么手动取消监听?unwatch有哪些实用场景和注意细节?

terry 3小时前 阅读数 33 #Vue
文章标签 Vue3 watchunwatch

Vue3的watch返回值真的就是unwatch函数吗?

直接回答第一个小问题前,得先回忆下或者搞明白watch在Vue3 Composition API中的核心机制——它其实是创建了一个“响应式副作用订阅”,这个订阅会随着依赖的响应式数据变化触发回调,而返回的正是用来彻底切断这个订阅连接的函数,不管你用的是普通watch、watchEffect还是watchPostEffect,这套逻辑都是通用的。

举个最基础的普通watch例子,你就能秒懂怎么用:

import { ref, watch } from 'vue'
const count = ref(0)
// 这里的stopCountWatch就是官方文档说的“清理函数”“取消监听函数”,我们习惯叫unwatch
const stopCountWatch = watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变成了${newVal}`)
})
// 随便触发几次监听
count.value++
count.value++
// 手动调用unwatch,彻底取消订阅
stopCountWatch()
// 再改count,控制台不会有输出了
count.value++

看到了吧?就这么简单一步——存返回值、调用返回值——就能取消监听,不过这里要提个容易混淆的点:有同学可能会和watch回调里的“清理回调参数”搞混,那个参数是用来清理单次监听执行时产生的临时副作用的(比如取消未完成的网络请求、移除事件监听器、清除定时器),不是用来取消整个watch订阅的,这俩完全是两码事,千万别搞反。

什么情况下才需要手动用unwatch?别瞎取消浪费性能

很多刚接触Vue3 Composition API的同学会觉得“所有watch都要手动取消才安全”,其实不然——Vue3默认帮我们做了一些生命周期的绑定,大多数情况下不需要手动取消,滥用反而会增加代码复杂度,甚至可能提前取消有用的监听,那什么时候才必须/值得用unwatch呢?我整理了4个非常高频的实用场景:

场景1:监听是临时触发的,用完就没用了

比如电商网站的“限时秒杀价格倒计时结束,弹出‘秒杀结束’提示框”这个需求——倒计时组件可能有个全局的秒杀倒计时变量,当商品详情页打开且秒杀未结束时,我们需要监听这个全局变量,等它变成0时弹出提示;但如果用户在倒计时没结束时就关闭了商品详情页,或者商品本来就不在秒杀时段,那这个监听就完全没必要存在了,这时候就可以用unwatch:

import { ref, computed, watch, onUnmounted } from 'vue'
import { useGlobalSeckillStore } from '@/stores/seckill' // 假设用Pinia管理全局秒杀状态
const seckillStore = useGlobalSeckillStore()
const isSeckilling = computed(() => seckillStore.currentProductId === productId.value && seckillStore.countdown > 0)
let stopSeckillWatch = null
// 只有秒杀进行中才创建监听
if (isSeckilling.value) {
  stopSeckillWatch = watch(() => seckillStore.countdown, (newVal) => {
    if (newVal <= 0) {
      showSeckillEndModal.value = true
      // 倒计时结束弹窗后,这个监听也没用了,顺手取消
      stopSeckillWatch?.()
    }
  })
}
// 组件卸载时,不管监听还在不在,都执行一次取消(防御性编程)
onUnmounted(() => {
  stopSeckillWatch?.()
})

这里加了onUnmounted里的取消是为了更安全——如果用户没等倒计时结束就关了详情页,组件虽然销毁了,但如果全局秒杀状态还在,这个订阅会不会一直存在内存里?很多同学可能会说“Vue3的watch默认绑定到当前组件的生命周期,组件卸载会自动取消”,没错,在setup()顶层或者setup的响应式函数块(比如computed的回调不算,但watch本身的创建是在顶层或setup里直接调用的)中创建的watch,确实会自动绑定到当前组件的effectScope,组件卸载时自动清理;但如果你的watch是在异步回调(比如setTimeout、fetch.then、点击事件回调)中创建的,或者是在父组件传递给子组件的函数中创建的,或者是在自己创建的独立effectScope中创建的,那Vue3就不会自动帮你清理了,这时候必须手动调用unwatch,否则会造成内存泄漏——这也是场景4要讲的核心。

场景2:监听的触发条件非常严格,满足一次后就不需要再监听了

这个场景其实刚才的秒杀例子已经用到了一部分——比如表单提交前的“表单字段是否全为必填”监听,我们只需要在第一次提交失败前监听字段变化,一旦提交成功了,这个监听就没用了;或者是“用户登录后,获取一次用户积分就停止监听登录状态”这种需求,都可以用这个思路。

举个表单提交的例子:

import { ref, reactive, watch } from 'vue'
const form = reactive({
  username: '',
  password: '',
  email: ''
})
const submitBtnDisabled = ref(true)
let stopFormWatch = null
// 只在表单初始化且未提交过的时候创建监听
stopFormWatch = watch(form, (newForm) => {
  submitBtnDisabled.value = !newForm.username || !newForm.password || !newForm.email
}, { immediate: true }) // immediate:true让初始化时也检查一次
const handleSubmit = async () => {
  try {
    await submitFormApi(form)
    // 提交成功!取消监听,节省资源
    stopFormWatch?.()
    submitBtnDisabled.value = true // 重置提交按钮状态
    resetForm() // 重置表单
  } catch (error) {
    console.error('提交失败', error)
  }
}

场景3:需要动态切换监听的数据源或配置,不想创建多个订阅

比如用户可以在设置里切换“实时同步数据的频率”——本来是监听lastSyncTime每30秒触发一次,用户改成了每分钟,这时候我们需要先取消原来的订阅,再用新的immediate和flush(虽然flush一般不用改,但假设这里也需要改的话)配置创建新的订阅:

import { ref, watch } from 'vue'
const lastSyncTime = ref(Date.now())
const syncInterval = ref(30000) // 默认30秒
let timer = null
let stopSyncWatch = null
// 封装一个创建同步监听的函数
const createSyncWatch = () => {
  stopSyncWatch?.() // 先取消旧的订阅,防止重复监听
  stopSyncWatch = watch(lastSyncTime, () => {
    syncDataApi() // 调用同步数据的API
    // 重置定时器
    clearInterval(timer)
    timer = setInterval(() => {
      lastSyncTime.value = Date.now()
    }, syncInterval.value)
  }, { immediate: true })
}
// 初始化时创建监听
createSyncWatch()
// 监听同步频率的变化,动态切换
watch(syncInterval, createSyncWatch)
// 组件卸载时清理所有定时器和监听
onUnmounted(() => {
  clearInterval(timer)
  stopSyncWatch?.()
})

场景4:watch创建在异步回调/独立effectScope/外部函数中,Vue3不会自动清理

刚才在场景1里提到过这个问题,这里单独拿出来举个例子,让大家更清楚:

import { ref, watch, effectScope, onUnmounted } from 'vue'
const count = ref(0)
let stopAsyncWatch = null
let stopScopeWatch = null
// 情况1:创建在setTimeout异步回调中
setTimeout(() => {
  stopAsyncWatch = watch(count, (newVal) => {
    console.log('异步回调中创建的watch触发了:', newVal)
  })
  // 此时如果没有手动取消,就算组件卸载了,这个订阅依然存在!
}, 1000)
// 情况2:创建在自己创建的独立effectScope中
const independentScope = effectScope()
independentScope.run(() => {
  stopScopeWatch = watch(count, (newVal) => {
    console.log('独立effectScope中创建的watch触发了:', newVal)
  })
})
// 组件卸载时,必须手动取消这两个订阅
onUnmounted(() => {
  stopAsyncWatch?.()
  // 独立effectScope也可以直接调用scope.stop(),会清理里面所有的响应式副作用
  // independentScope.stop() // 和调用stopScopeWatch效果一样,但更省事
  stopScopeWatch?.()
})

用unwatch时的3个注意细节,避坑99%的问题

虽然unwatch看起来很简单,但用不好还是会出问题,我整理了3个最常见的坑:

细节1:多次调用同一个unwatch函数不会报错,但也不会有任何效果

Vue3的官方设计就是这样的——unwatch函数会在第一次调用后内部标记为“已停止”,之后再调用直接返回undefined,不会触发任何副作用,也不会报错,所以大家可以放心地多次调用(比如在回调里和onUnmounted里都写一次),这也是一种很好的防御性编程习惯。

细节2:取消watch后,原来的依赖追踪关系会完全断开,不会再恢复

很多同学会问“取消watch后能不能再恢复?”,答案是不能直接恢复——你只能重新调用watch/watchEffect创建一个新的订阅,就像场景3里的createSyncWatch函数那样,这是因为unwatch函数会把内部的effect对象标记为“inactive”,并且清空所有的依赖追踪关系,整个订阅就彻底失效了。

细节3:不要在watch的回调里直接调用自己的unwatch函数,除非你确定只需要触发一次

虽然刚才的秒杀例子和表单提交例子都是这么做的,但要注意——如果你的watch配置了deep:true或者immediate:true,在immediate触发时调用unwatch会不会有问题?答案是不会有问题,Vue3官方已经处理了这种情况——在immediate执行的回调里调用unwatch,会立即停止后续的监听,不会触发任何副作用,但如果你的逻辑不是“只触发一次”,千万不要这么做,否则会导致监听提前失效。

Vue3 watch unwatch的核心要点

  1. 怎么取消? 存watch/watchEffect/watchPostEffect的返回值,调用返回值即可。
  2. 什么时候取消? 临时监听、满足一次后不需要、动态切换配置、异步/独立scope创建的监听。
  3. 避坑点: 多次调用没问题、不能直接恢复、在回调里调用要谨慎。

最后再强调一遍:不要滥用unwatch! 大多数情况下,Vue3默认绑定到当前组件effectScope的watch会自动清理,手动取消只会增加代码复杂度,甚至可能出错,只有在刚才提到的4个高频场景下,才需要考虑使用unwatch。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门