4、watch与watchEffect:监听数据变化

数据变化了,我得知道。这是前端开发里最基础的需求之一。

在 Options API 时代,我们用 watch 选项。到了组合式 API,Vue 给了我们两个选择:watchwatchEffect。很多人刚接触时容易搞混,其实它们各有各的脾气。

watch:精准监听,懒执行

watch 的特点,说白了就是「你让我看谁,我就看谁」。它不会一上来就执行,只有你指定的数据变了,它才干活。

import { ref, watch } from 'vue'

const count = ref(0)

// 只有 count 变化时才会触发
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变成了 ${newVal}`)
})

我个人习惯用 watch 来处理那些「需要旧值做对比」的场景。比如表单编辑时,用户改了某个字段,我想知道改之前是什么。

核心要点:watch 默认是懒执行的。它不会在组件初始化时运行,只有监听的数据源发生变化后才会触发回调。

watchEffect:自动追踪,立即执行

watchEffect 就有点「智能」了。你不需要告诉它要监听谁,它会自己分析回调函数里用了哪些响应式数据,然后自动追踪它们。

import { ref, watchEffect } from 'vue'

const keyword = ref('')
const pageSize = ref(20)

watchEffect(() => {
  // 这里用了 keyword 和 pageSize
  // 它们任何一个变了,这个函数都会重新执行
  console.log(`搜索关键词:${keyword.value},每页条数:${pageSize.value}`)
})

注意,watchEffect 一上来就会执行一次。这在初始化时特别有用。我在项目中遇到过这样一个场景:页面加载时需要根据 URL 参数初始化筛选条件,用 watchEffect 就省去了手动调用的麻烦。

小技巧:如果你需要在组件挂载时自动执行一次,并且后续自动追踪变化,用 watchEffect 准没错。

两者对比:什么时候用哪个?

特性 watch watchEffect
执行时机 懒执行,数据变化后才触发 立即执行,且自动追踪依赖
是否需要指定源 需要明确指定监听的数据 自动分析回调中的响应式数据
能否获取旧值 可以(newVal, oldVal) 不能获取旧值
适用场景 需要旧值对比、精确控制监听源 不需要旧值、需要自动追踪多个依赖

你想想看,如果只是要「数据变了就做点事」,而且不需要旧值,那 watchEffect 写起来更简洁。但如果你需要精确控制,或者需要对比新旧值,那就老老实实用 watch

如何停止监听?

嗯,这里要注意。不管是 watch 还是 watchEffect,它们都会返回一个停止函数。调用它就能手动停止监听。

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)

// watch 的停止
const stopWatch = watch(count, (newVal) => {
  console.log('watch 触发:', newVal)
})

// watchEffect 的停止
const stopEffect = watchEffect(() => {
  console.log('watchEffect 触发:', count.value)
})

// 5秒后停止所有监听
setTimeout(() => {
  stopWatch()
  stopEffect()
  console.log('监听已停止')
}, 5000)

我曾经踩过一个坑:在组件里用 watchEffect 监听了一个全局状态,结果组件销毁了,监听还在跑。后来排查才发现,原来 watchEffect 默认会绑定到当前组件实例的生命周期。但如果你在组件外部或者异步回调里创建它,就得手动停止。

避坑指南:在组件卸载时,Vue 会自动清理组件内创建的 watch 和 watchEffect。但如果你在 setTimeout、Promise 回调或全局事件中创建它们,一定要手动调用停止函数,否则会造成内存泄漏。

watch 的更多玩法

watch 其实还能监听多个数据源,或者监听一个 getter 函数。这些在复杂场景下特别有用。

import { ref, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 监听多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 ${oldFirst}${oldLast} 变成了 ${newFirst}${newLast}`)
})

// 监听 getter 函数
const fullName = ref('')
watch(
  () => `${firstName.value}${lastName.value}`,
  (newVal) => {
    fullName.value = newVal
  }
)

我个人习惯在表单校验场景用 getter 形式。比如监听「表单数据是否完整」,用一个计算属性或者 getter 来判断,比监听多个字段要清爽得多。

watchEffect 的进阶用法

watchEffect 还有一个隐藏技能:它可以在回调里接收一个 onCleanup 函数,用来处理副作用清理。

import { ref, watchEffect } from 'vue'

const userId = ref(1)

watchEffect((onCleanup) => {
  // 模拟异步请求
  const controller = new AbortController()
  
  fetch(`/api/user/${userId.value}`, {
    signal: controller.signal
  }).then(res => res.json())
    .then(data => {
      console.log('用户数据:', data)
    })

  // 下次执行前,先取消上一次的请求
  onCleanup(() => {
    controller.abort()
    console.log('取消了上一次请求')
  })
})

这个 onCleanup 在防抖、取消请求、清理定时器时特别好用。我做过一个搜索框,用户输入时频繁发请求,用 onCleanup 取消上一次请求,既省流量又避免数据错乱。

总结一下:

  • 需要旧值、精确控制 → 用 watch
  • 自动追踪、立即执行、不需要旧值 → 用 watchEffect
  • 组件销毁时,Vue 会自动清理,但异步场景要手动停止
  • watchEffect 的 onCleanup 是处理副作用的利器

说白了,这两个 API 没有谁优谁劣,关键看场景。你用得多了,自然就知道什么时候该用哪个。刚开始不熟悉的话,先从 watch 入手,它更直观。等你习惯了响应式的思维方式,watchEffect 会让你写得更爽。