04 watch 与 Vue 响应式数据流

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 往往更容易维护。

九、computedwatch 如何选择

使用 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();
  }
});

这段逻辑需要注意:

  1. 先停止旧模式任务;
  2. 再加载新模式数据;
  3. 最后启动新任务;
  4. 避免同时存在两个定时器。

十二、避免高频重复更新

如果多个响应式状态在同一时刻连续变化,直接调用昂贵更新可能重复执行。

可以借助 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 监听关键变化
       ↓
执行请求、同步索引或更新外部模块

下一章将学习生命周期,理解这些任务应该在页面的哪个阶段启动和清理。

相关推荐
kyriewen3 小时前
我排查了一个React内存泄漏——罪魁祸首是这3个被忽略的清理函数
前端·javascript·面试
IT_陈寒4 小时前
我又被JavaScript的隐式类型转换坑了
前端·人工智能·后端
其美杰布-富贵-李4 小时前
03 ref、reactive 与 computed 响应式数据
前端·javascript·vue.js
OpenTiny社区4 小时前
GenUI SDK v1.3.0 发布|多框架兼容,一键换物料,渲染器 & 演练场全面增强!
前端·ai编程
用户938515635074 小时前
useRef + Web Worker 实战:React 如何优雅地拥抱多线程
前端·javascript·react.js
嘟嘟07174 小时前
useRef + Web Worker:React 中的耗时计算不卡页面
javascript·vue.js
mONESY4 小时前
React Router v7 完全实战指南:从入门到鉴权,一套代码全搞定
javascript
万敏4 小时前
Vue3 全栈实战第五周:Vitest 单元测试从零搭建实战记录
vue.js·node.js·全栈