watch 与 Vue 响应式数据流
一、为什么需要watch
computed适合根据已有状态计算结果,例如:
constisRealtimeMode=computed(()=>displayMode.value==='realtime');但有些需求不是“得到一个新值”,而是“状态变化后执行一项操作”:
- 切换模式后重新请求数据;
- 当前帧变化后更新三维场景;
- 路由参数变化后加载新的炉次;
- 播放索引越界时自动修正;
- 用户开启开关后启动定时器。
这类需求可以使用watch()。
二、监听一个ref
import{ref,watch}from'vue';constdisplayMode=ref<'realtime'|'playback'>('playback');watch(displayMode,(newMode,oldMode)=>{console.log('旧模式:',oldMode);console.log('新模式:',newMode);});当displayMode.value改变时,回调函数执行。
三、监听计算属性
constisRealtimeMode=computed(()=>displayMode.value==='realtime');watch(isRealtimeMode,(isRealtime)=>{if(isRealtime){startPolling();}else{stopPolling();}});只要传入的是 Vue 可以追踪的响应式来源,就能够监听。
四、监听对象属性
不能直接传普通值:
watch(state.count,()=>{});// 错误或无法正确监听应传入 getter:
watch(()=>state.count,(count)=>{console.log(count);});路由参数通常也采用这种方式:
watch(()=>route.query.heat_id,(newHeatId)=>{loadHeatData(String(newHeatId||''));});五、同时监听多个来源
watch([temperature,oxygenFlow,tiltAngle],([newTemperature,newFlow,newTilt])=>{console.log(newTemperature,newFlow,newTilt);updateScene();});大型可视化页面中,多个状态都会影响场景。可以将它们放进同一个数组:
watch([currentTime,riskClass,temperature,oxygenFlow,shellOpacity],()=>{scheduleSceneUpdate();});这样可以明确表达:这些状态变化后,都需要更新外部渲染模块。
六、immediate:立即执行一次
默认情况下,watch只在数据变化后执行。
watch(heatId,(id)=>{loadHeatData(id);},{immediate:true});immediate: true表示创建监听器时立即执行一次。
它适合“首次加载和后续变化使用同一逻辑”的场景。
不过,如果项目已经在onMounted()中明确加载数据,就不一定需要再使用immediate,避免重复请求。
七、深度监听deep
constform=reactive({user:{name:'',age:0}});watch(form,()=>{console.log('表单发生变化');},{deep:true});深度监听会追踪嵌套属性,可能增加开销。
优先监听真正关心的字段:
watch(()=>form.user.name,(name)=>{console.log(name);});不要因为“不确定改了哪个字段”就对大型对象一律使用deep: true。
八、watchEffect()
watchEffect会立即执行,并自动收集回调中读取的响应式依赖:
watchEffect(()=>{console.log('当前模式:',displayMode.value);console.log('当前炉次:',heatId.value);});区别可以简单理解为:
watch 明确指定监听谁,可获得新旧值 watchEffect 自动收集依赖,立即执行真实业务中,如果依赖和触发条件需要非常明确,watch往往更容易维护。
九、computed和watch如何选择
使用computed
当目标是得到一个新值:
constprogressText=computed(()=>`${playbackIndex.value+1}/${frames.value.length}`);使用watch
当目标是执行副作用:
watch(playbackIndex,()=>{updateExternalRenderer();});判断规则:
“这个结果是什么?” → computed “变化后要做什么?” → watch十、索引越界修正案例
回放数据重新加载后,帧数可能变少。如果当前索引超过数组范围,需要自动修正:
watch(playbackGroups,(groups)=>{constmaxIndex=Math.max(groups.length-1,0);if(playbackIndex.value>maxIndex){playbackIndex.value=maxIndex;}});这里不是在计算一个显示值,而是在数据变化后修正另一个状态,因此适合使用watch。
十一、模式切换案例
watch(displayMode,async(mode)=>{stopPlayback();stopRealtimePolling();if(mode==='realtime'){awaitloadSnapshot();startRealtimePolling();}else{awaitloadPlayback();}});这段逻辑需要注意:
- 先停止旧模式任务;
- 再加载新模式数据;
- 最后启动新任务;
- 避免同时存在两个定时器。
十二、避免高频重复更新
如果多个响应式状态在同一时刻连续变化,直接调用昂贵更新可能重复执行。
可以借助requestAnimationFrame合并:
letupdateFrame:number|null=null;functionscheduleSceneUpdate(){if(updateFrame!==null){return;}updateFrame=requestAnimationFrame(()=>{updateFrame=null;updateScene();});}监听器只负责安排更新:
watch([temperature,oxygenFlow,riskClass],scheduleSceneUpdate);同一浏览器帧内无论触发多少次,都只执行一次真正更新。
十三、异步监听与过期结果
下面的监听可能出现请求竞争:
watch(heatId,async(id)=>{constdata=awaitgetHeatDetail(id);heat.value=data;});用户快速切换heatId时,旧请求可能最后返回并覆盖新数据。
一种简单处理方式是记录请求编号:
letrequestId=0;watch(heatId,async(id)=>{constcurrentId=++requestId;constdata=awaitgetHeatDetail(id);if(currentId!==requestId){return;}heat.value=data;});更完整的项目可以使用取消请求、请求门控或专门的数据请求库。
十四、停止监听
watch会返回停止函数:
conststop=watch(count,()=>{console.log(count.value);});stop();在组件setup中创建的普通监听器会随组件卸载自动停止。但动态创建、跨组件保存或外部作用域中的监听仍需谨慎管理。
十五、常见误区
1. 用watch计算显示文本
watch(count,()=>{countText.value=`当前数量:${count.value}`;});更适合:
constcountText=computed(()=>`当前数量:${count.value}`);2. 在多个监听器中修改同一组状态
容易形成循环触发和难以定位的数据链。
3. 监听整个大型对象
优先监听实际需要的字段。
4. 模式切换时只启动新任务,不停止旧任务
会产生重复轮询、重复播放或资源泄漏。
十六、本章小结
大型 Vue 页面的典型响应式链是:
接口结果进入 ref ↓ computed 整理当前状态 ↓ watch 监听关键变化 ↓ 执行请求、同步索引或更新外部模块下一章将学习生命周期,理解这些任务应该在页面的哪个阶段启动和清理。