一、Vue3的watch最基础的用法是啥?
p>不少刚接触Vue3的同学,对watch的用法总是有点迷糊——明明看了文档,实际写代码时要么监听不到变化,要么触发时机不对,今天咱就用问答的形式,把Vue3 watch的常见用法、容易踩的坑,还有实战里的技巧掰开了讲清楚,帮你彻底搞懂这块知识点。
想监听数据变化然后执行逻辑,得先搞清楚watch的“监听源”“回调函数”“配置项”这三部分逻辑。
先看监听ref定义的基本类型(比如字符串、数字),假设我们用ref
声明了一个计数变量:
import { ref, watch } from 'vue' const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`count从${oldVal}变成了${newVal}`) }) // 当执行count.value++时,回调就会触发
要是监听reactive定义的对象,直接传整个对象容易踩坑(后面讲特殊场景),更常见的是监听对象的某个属性,这时候得用“函数返回要监听的属性”:
import { reactive, watch } from 'vue' const user = reactive({ name: '小明', age: 18 }) watch( () => user.age, // 用函数返回要监听的属性 (newAge, oldAge) => { console.log(`年龄从${oldAge}涨到了${newAge}`) } ) // 当执行user.age = 19时,回调触发
watch的第三个参数是配置项,比如immediate: true
能让回调在监听开始时就执行一次(默认是“数据变化才执行”,也就是“惰性执行”):
watch( () => user.name, (newName) => { console.log('名字变化了:', newName) }, { immediate: true } // 页面加载时先执行一次,不管name有没有变 )
watch和watchEffect有啥不一样?
很多同学会把这俩搞混,其实核心区别在“依赖追踪方式”和“执行时机”上。
先看依赖追踪:
- watch得手动指定要监听的源(比如上面的
count
、()=>user.age
),Vue只盯你指定的这几个数据; - watchEffect不用指定源,它会自动收集函数里用到的响应式数据作为依赖。
import { watchEffect } from 'vue' watchEffect(() => { console.log(`当前名字是${user.name},年龄${user.age}`) }) // 这里user.name和user.age都会被自动追踪,任意一个变化,回调就执行
再看执行时机:
- watch默认是“惰性”的,只有监听的源变化了才执行回调;
- watchEffect默认“立即执行”一次(收集依赖后,不管数据变没变,先执行一遍),之后依赖变化再执行。
所以场景选择很明确:
- 想精准控制监听对象,或者需要拿到“变化前后的值”(watch能拿到
oldVal
),用watch; - 只是想处理一堆响应式数据的副作用(比如发请求、操作DOM),不需要关心旧值,用watchEffect更简洁。
怎么同时监听多个数据的变化?
当需要多个数据任意一个变化都触发逻辑时,把监听源写成数组就行,这时候回调的新值、旧值也会对应成数组。
举个例子,同时监听用户名和年龄:
const user = reactive({ name: '小明', age: 18 }) watch( [() => user.name, () => user.age], // 数组里放多个监听源 ([newName, newAge], [oldName, oldAge]) => { // 新值、旧值也是数组 console.log(`名字从${oldName}变${newName},年龄从${oldAge}变${newAge}`) } ) // 当user.name或user.age变化时,回调都会触发
如果是ref的情况,直接把ref变量放进数组:
const count = ref(0) const msg = ref('hello') watch( [count, msg], ([newCount, newMsg], [oldCount, oldMsg]) => { // 处理新老值... } )
深度监听要注意啥?
当监听的对象嵌套了对象或数组时,默认情况下Vue只能监听到“对象被整个替换”的变化,内部属性变了监听不到,这时候得开deep: true
。
比如监听一个嵌套对象:
const info = reactive({ base: { city: '北京', area: '朝阳' }, hobbies: ['篮球', '音乐'] }) // 错误示范:没开deep,修改base.city监听不到 watch(() => info.base, (newVal) => { console.log('base变化了?', newVal) // 只有info.base = { ... } 才会触发 }) // 正确示范:开deep watch( () => info.base, (newVal) => { console.log('base内部属性变了!', newVal) }, { deep: true } ) // 现在修改info.base.city = '上海',回调会触发
但deep: true
有性能代价(Vue要递归遍历对象所有属性),所以尽量别对整个大对象开deep,而是监听具体的深层属性:
// 更高效:直接监听city watch( () => info.base.city, (newCity) => { ... } )
如果监听的是ref包裹的对象(比如const obj = ref({ a: { b: 1 } })
),要注意:watch(obj, { deep: true })
监听不到obj.value.a.b
的变化!因为ref的.value
才是响应式对象,所以正确做法是监听obj.value
或者具体属性:
watch(() => obj.value, { deep: true }, () => { ... }) // 或者 watch(() => obj.value.a.b, () => { ... })
监听reactive对象有啥特殊点?
reactive创建的是“响应式代理对象”,直接把整个reactive对象当监听源时,有两个容易踩的坑:
-
新值和旧值是同一个引用
因为reactive对象是Proxy,修改内部属性不会替换整个对象,所以watch的回调里newVal
和oldVal
指向同一个对象:const user = reactive({ name: '小明' }) watch(user, (newVal, oldVal) => { console.log(newVal === oldVal) // 输出true }) user.name = '小红' // 触发回调,但newVal和oldVal是同一个对象
如果需要区分变化前后,得结合
deep: true
+ 手动记录旧值,或者监听具体属性(更推荐)。 -
数组元素的“非替换修改”监听不到
如果监听reactive声明的数组,用索引修改元素(比如arr[0] = 2
)或修改length(比如arr.length = 2
),默认监听不到,必须开deep: true
,举个例子:const list = reactive([{ id: 1, name: 'a' }]) watch(list, () => { ... }, { deep: true }) list[0].name = 'b' // 开deep才会触发 list[0] = { id:1, name: 'c' } // 替换元素,不管deep都会触发(因为整个元素被替换)
怎么停止watch的监听?
有时候我们需要在组件卸载前或者满足某个条件后停止监听(比如弹窗关闭后不再监听数据),这时候要用到watch返回的“停止函数”。
在<script setup>
或setup
函数里,watch调用后会返回一个函数,执行它就能停止监听:
<script setup> import { ref, watch, onUnmounted } from 'vue' const count = ref(0) // 创建watch并保存停止函数 const stopWatch = watch(count, (newVal) => { console.log('count变化:', newVal) }) // 比如3秒后停止监听 setTimeout(() => { stopWatch() // 执行后,count再变化也不会触发回调了 }, 3000) // 组件卸载时停止(script setup里onUnmounted自动可用) onUnmounted(() => { stopWatch() }) </script>
如果在同一个组件里多次创建watch,记得给每个watch都配置停止逻辑,避免内存泄漏。
监听数组有啥要注意的?
数组的修改分“变异方法”(push/pop等)和“替换数组”(重新赋值)两种,watch对这两种情况的监听逻辑不一样。
- 用变异方法修改数组(比如push、pop、splice):
如果数组是reactive声明的,或者ref包裹的数组(const arr = ref([1,2,3])
),调用arr.value.push(4)
,watch是能监听到的,但如果是用索引修改元素(arr.value[0] = 10
)或者修改length(arr.value.length = 2
),默认监听不到,必须开deep: true
。
举个例子:
const arr = ref([{ id: 1, name: 'a' }]) // 情况1:用push watch(arr, () => { console.log('arr变化了') }) arr.value.push({ id: 2, name: 'b' }) // 触发回调 // 情况2:用索引改元素内部属性 watch(arr, () => { ... }, { deep: true }) arr.value[0].name = 'aa' // 开deep才触发 // 情况3:替换数组 watch(arr, () => { ... }) arr.value = [1,2,3] // 触发回调(因为整个数组被替换,ref的value变化了)
- 数组里是原始类型(1,2,3]) vs 对象/数组:
如果数组里是数字、字符串这些原始类型,修改元素(arr[0] = 5
)属于“替换元素”,watch能监听到;但如果是对象,修改对象内部属性(arr[0].name = 'x'
),就需要开deep
了。