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 对比
| 特性 | watch | watchEffect |
|---|---|---|
| 执行时机 | 默认惰性 | 立即执行 |
| 依赖收集 | 显式指定 | 自动收集 |
| 旧值 | 可获取 | 无法获取 |
| 适用场景 | 需新旧值对比、精确控制 | 简单副作用 |
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})七、常见坑点总结
- 侦听 reactive 对象直接传参会失效:必须用 getter,否则新旧值相同。
- ref 对象的
.value:侦听 ref 本身传ref,侦听属性传() => ref.value.xxx。 - deep 性能问题:大对象深度侦听开销大,优先用 getter 精确侦听。
- 数组/对象引用变化:
watch侦听的是引用,直接修改属性需deep: true。 - 异步创建的 watch 不会自动清理:需要手动调用停止函数,或用
onUnmounted清理。 watchEffect无法获取旧值:需要新旧值对比时用watch。flush: 'sync'慎用:频繁触发会导致性能问题。
八、速查表
| 场景 | 写法 |
|---|---|
| 侦听单个 ref | watch(ref, cb) |
| 侦听 reactive 属性 | watch(() => obj.key, cb) |
| 侦听整个 reactive | watch(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 就不传对象,异步副作用记得清理。