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

一、Vue3的watch最基础的用法是啥?

terry 2小时前 阅读数 6 #Vue
文章标签 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对象当监听源时,有两个容易踩的坑:

  1. 新值和旧值是同一个引用
    因为reactive对象是Proxy,修改内部属性不会替换整个对象,所以watch的回调里newValoldVal指向同一个对象:

    const user = reactive({ name: '小明' })  
    watch(user, (newVal, oldVal) => {  
    console.log(newVal === oldVal) // 输出true  
    })  
    user.name = '小红' // 触发回调,但newVal和oldVal是同一个对象  

    如果需要区分变化前后,得结合deep: true+ 手动记录旧值,或者监听具体属性(更推荐)。

  2. 数组元素的“非替换修改”监听不到
    如果监听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对这两种情况的监听逻辑不一样。

  1. 变异方法修改数组(比如push、pop、splice):
    如果数组是reactive声明的,或者ref包裹的数组(const arr = ref([1,2,3])),调用arr.value.push(4),watch是能监听到的,但如果是用索引修改元素arr.value[0] = 10)或者修改lengtharr.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. 数组里是原始类型(1,2,3]) vs 对象/数组
    如果数组里是数字、字符串这些原始类型,修改元素(arr[0] = 5)属于“替换元素”,watch能监听到;但如果是对象,修改对象内部属性(arr[0].name = 'x'),就需要开deep了。

在setup和