4、watch与watchEffect:监听数据变化
数据变化了,我得知道。这是前端开发里最基础的需求之一。
在 Options API 时代,我们用 watch 选项。到了组合式 API,Vue 给了我们两个选择:watch 和 watchEffect。很多人刚接触时容易搞混,其实它们各有各的脾气。
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 会让你写得更爽。