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