☰
Vue 3 Watch 监视完全指南
2026/9/28 20:56:22 网站建设 项目流程

Vue 3 Watch 监视完全指南

一、watch 基础概念

watch用于侦听响应式数据源的变化,并在变化时执行副作用函数。它默认是惰性的(不会立即执行),且能获取新值和旧值。

基本语法

watch(source,callback,options)
  • source:侦听源(ref、reactive、getter 函数、数组)
  • callback:(newVal, oldVal, onCleanup) => {}
  • options:配置项(immediate、deep、flush、once 等)

二、侦听不同数据源

1. 侦听 ref

<script setup> import { ref, watch } from 'vue' const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`count: ${oldVal} -> ${newVal}`) }) count.value++ // 输出: count: 0 -> 1 </script>

2. 侦听 reactive 对象

⚠️注意:侦听 reactive 对象时,必须使用 getter 函数,否则会隐式创建深度侦听,且新值等于旧值。

import{reactive,watch}from'vue'conststate=reactive({name:'Tom',age:18})// ❌ 错误:直接传 reactive,新旧值相同watch(state,(newVal,oldVal)=>{console.log(newVal===oldVal)// true(因为都是同一个代理对象)})// ✅ 正确:使用 getterwatch(()=>state.name,(newVal,oldVal)=>{console.log(`name:${oldVal}->${newVal}`)})// ✅ 侦听整个 reactive 需要 deepwatch(()=>state,(newVal)=>console.log(newVal),{deep:true})

3. 侦听 getter 函数

constuser=reactive({first:'张',last:'三'})// 侦听组合值watch(()=>user.first+user.last,(newVal,oldVal)=>{console.log(`全名:${oldVal}->${newVal}`)})

4. 侦听多个源(数组)

consta=ref(1)constb=ref(2)watch([a,b],([newA,newB],[oldA,oldB])=>{console.log(`a:${oldA}->${newA}, b:${oldB}->${newB}`)})// 混合写法watch([a,()=>b.value*2],([newA,newDoubleB])=>{console.log(newA,newDoubleB)})

三、核心配置项 options

1.immediate—— 立即执行

constid=ref(1)watch(id,(newVal)=>{console.log('立即执行一次:',newVal)// 首次运行就输出 1},{immediate:true})

常用于组件初始化时根据 props 拉取数据。

2.deep—— 深度侦听

constobj=ref({nested:{count:0}})// ❌ 不加 deep,修改嵌套属性不会触发watch(obj,()=>console.log('触发'))obj.value.nested.count++// 不会触发// ✅ 加 deepwatch(obj,()=>console.log('触发'),{deep:true})obj.value.nested.count++// 触发

reactive 对象的特殊情况:直接侦听 reactive 对象时,Vue 会自动启用 deep:

conststate=reactive({nested:{count:0}})watch(state,()=>{console.log('自动深度侦听')})state.nested.count++// 触发

性能优化:使用 getter 精确侦听,避免 deep 全量遍历。

// ✅ 只侦听特定属性,性能更好watch(()=>obj.value.nested.count,()=>{})

3.flush—— 回调执行时机

值说明
'pre'(默认)组件更新前执行
'post'组件更新后执行
'sync'同步执行,变化立即触发
watch(source,cb,{flush:'post'})// 可访问更新后的 DOMwatch(source,cb,{flush:'sync'})// 谨慎使用,性能开销大

4.once—— 只执行一次(Vue 3.4+)

watch(source,(newVal)=>{console.log('只执行一次')// 变化后只触发一次},{once:true})

5.onCleanup—— 清理副作用

用于取消上一次的异步请求、定时器等,防止竞态问题。

watch(id,async(newId,oldId,onCleanup)=>{constcontroller=newAbortController()onCleanup(()=>controller.abort())// 下次触发前先取消上次请求constres=awaitfetch(`/api/user/${newId}`,{signal:controller.signal})constdata=awaitres.json()console.log(data)})

四、停止侦听

watch返回一个停止函数,调用它可手动停止侦听。

conststop=watch(count,()=>{console.log('count 变化了')})count.value++// 触发stop()// 停止侦听count.value++// 不再触发

注意:在setup()或<script setup>中创建的 watch,会自动绑定到组件实例,组件卸载时自动停止。只有在异步回调中创建的 watch 才需要手动停止。

// 异步中创建,需要手动清理setTimeout(()=>{conststop=watch(source,cb)onUnmounted(stop)},1000)

五、watchEffect —— 自动追踪

watchEffect会立即执行一次,并自动收集回调中用到的响应式依赖。

constcount=ref(0)constname=ref('Tom')watchEffect(()=>{// 自动追踪 count 和 nameconsole.log(`count=${count.value}, name=${name.value}`)})// 立即输出一次,之后任一变化都触发

watch vs watchEffect 对比

特性watchwatchEffect
执行时机默认惰性立即执行
依赖收集显式指定自动收集
旧值可获取无法获取
适用场景需新旧值对比、精确控制简单副作用

watchEffect 的停止与清理

conststop=watchEffect((onCleanup)=>{consttimer=setInterval(()=>{},1000)onCleanup(()=>clearInterval(timer))})stop()// 停止

watchPostEffect和watchSyncEffect

import{watchPostEffect,watchSyncEffect}from'vue'// flush: 'post'watchPostEffect(()=>{// DOM 更新后执行})// flush: 'sync'watchSyncEffect(()=>{// 同步执行})

六、实战案例

案例 1:搜索框防抖

<script setup> import { ref, watch } from 'vue' const keyword = ref('') const results = ref([]) let timer = null watch(keyword, (newVal) => { clearTimeout(timer) timer = setTimeout(async () => { if (!newVal) { results.value = [] return } const res = await fetch(`/api/search?q=${newVal}`) results.value = await res.json() }, 300) }) </script> <template> <input v-model="keyword" placeholder="搜索..." /> <ul> <li v-for="item in results" :key="item.id">{{ item.name }}</li> </ul> </template>

案例 2:侦听 props 变化

<script setup> import { watch, toRefs } from 'vue' const props = defineProps({ userId: Number }) // ✅ 推荐:getter 写法 watch( () => props.userId, async (newId) => { if (newId) { await loadUser(newId) } }, { immediate: true } ) </script>

案例 3:多源侦听 + 深度侦听

constform=reactive({name:'',address:{city:'',street:''}})watch([()=>form.name,()=>form.address.city],([newName,newCity],[oldName,oldCity])=>{console.log(`name:${oldName}->${newName}`)console.log(`city:${oldCity}->${newCity}`)},{immediate:true})

案例 4:避免竞态的异步请求

watch(()=>props.id,async(newId,oldId,onCleanup)=>{letcancelled=falseonCleanup(()=>(cancelled=true))constdata=awaitfetchData(newId)if(!cancelled){// 确保只渲染最新请求的结果result.value=data}},{immediate:true})

七、常见坑点总结

  1. 侦听 reactive 对象直接传参会失效:必须用 getter,否则新旧值相同。
  2. ref 对象的.value:侦听 ref 本身传ref,侦听属性传() => ref.value.xxx。
  3. deep 性能问题:大对象深度侦听开销大,优先用 getter 精确侦听。
  4. 数组/对象引用变化:watch侦听的是引用,直接修改属性需deep: true。
  5. 异步创建的 watch 不会自动清理:需要手动调用停止函数,或用onUnmounted清理。
  6. watchEffect无法获取旧值:需要新旧值对比时用watch。
  7. flush: 'sync'慎用:频繁触发会导致性能问题。

八、速查表

场景写法
侦听单个 refwatch(ref, cb)
侦听 reactive 属性watch(() => obj.key, cb)
侦听整个 reactivewatch(obj, cb)(自动 deep)
侦听多个源watch([a, b], cb)
立即执行{ immediate: true }
深度侦听{ deep: true }
DOM 更新后执行{ flush: 'post' }
只执行一次{ once: true }
自动追踪依赖watchEffect(fn)
停止侦听const stop = watch(...); stop()

掌握以上内容,即可应对 Vue 3 中绝大多数watch使用场景。核心原则:能精确侦听就不用 deep,能用 getter 就不传对象,异步副作用记得清理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询