Vue 3 的watch监听多个数据源:讲解如何同时监听多个响应式数据的变化

🎪 前端摸鱼匠:个人主页

🎒 个人专栏:《vue3入门到精通

🥇 没有好的理念,只有脚踏实地!


文章目录

      • [一、 为什么需要监听多个数据源?](#一、 为什么需要监听多个数据源?)
      • [二、 `watch` 的核心基础:温故而知新](#二、 watch 的核心基础:温故而知新)
        • [2.1 `watch` 的本质:一个精确的观察者](#2.1 watch 的本质:一个精确的观察者)
        • [2.2 `watchEffect`:一个自动追踪的"懒人"观察者](#2.2 watchEffect:一个自动追踪的“懒人”观察者)
        • [2.3 `watch` vs `watchEffect`:一张表格看懂核心区别](#2.3 watch vs watchEffect:一张表格看懂核心区别)
      • [三、 核心技法:`watch` 如何监听多个数据源](#三、 核心技法:watch 如何监听多个数据源)
        • [3.1 基础语法:数组作为第一个参数](#3.1 基础语法:数组作为第一个参数)
        • [3.2 监听 `reactive` 对象的多个属性](#3.2 监听 reactive 对象的多个属性)
        • [3.3 混搭监听:`ref`、`reactive` Getter 一起上](#3.3 混搭监听:refreactive Getter 一起上)
      • [四、 高阶玩法:`watch` 的配置项与生命周期](#四、 高阶玩法:watch 的配置项与生命周期)
        • [4.1 配置项详解:`deep`, `immediate`, `flush`](#4.1 配置项详解:deep, immediate, flush)
          • [4.1.1 `deep: true` ------ 深入敌后](#4.1.1 deep: true —— 深入敌后)
          • [4.1.2 `immediate: true` ------ 立即执行](#4.1.2 immediate: true —— 立即执行)
          • [4.1.3 `flush: 'pre' | 'post' | 'sync'` ------ 精确控制执行时机](#4.1.3 flush: 'pre' | 'post' | 'sync' —— 精确控制执行时机)
        • [4.2 副作用的清理:`onCleanup` 函数](#4.2 副作用的清理:onCleanup 函数)
        • [4.3 手动停止监听:`stopWatch` 函数](#4.3 手动停止监听:stopWatch 函数)
      • [五、 实战应用场景与模式](#五、 实战应用场景与模式)
        • [5.1 场景一:实时表单验证与联动](#5.1 场景一:实时表单验证与联动)
        • [5.2 场景二:带防抖的智能搜索](#5.2 场景二:带防抖的智能搜索)
        • [5.3 场景三:数据分页与筛选器联动](#5.3 场景三:数据分页与筛选器联动)
      • [六、 常见陷阱与最佳实践](#六、 常见陷阱与最佳实践)
        • [6.1 陷阱一:无限循环](#6.1 陷阱一:无限循环)
        • [6.2 陷阱二:响应式丢失](#6.2 陷阱二:响应式丢失)
        • [6.3 陷阱三:性能滥用](#6.3 陷阱三:性能滥用)
      • [七、 总结:成为 `watch` 大师](#七、 总结:成为 watch 大师)

一、 为什么需要监听多个数据源?

在正式撸代码之前,我们先花点时间聊聊"为什么"。理解了背后的动机,学习具体的技术点时就会更有方向感。

在组件化的开发思想中,每个组件都像一个独立的小王国,它有自己的状态(数据)和逻辑。然而,这些小王国并非完全孤立的,它们之间需要通信,需要对内部或外部的变化做出响应。watch 就是 Vue 提供给我们的一种响应式"传感器",它能感知数据的变化,并执行我们预先定义好的副作用操作。

那么,为什么这个"传感器"需要同时感知多个信号呢?

1. 复杂的表单联动

想象一个用户注册表单,其中有"密码"和"确认密码"两个输入框。我们需要实时判断:

  • 两次输入的密码是否一致?
  • 密码的强度是否达标?

这里的判断逻辑就依赖于两个数据源:passwordconfirmPassword。任何一个输入框内容的变化,都需要重新进行校验。如果只用两个独立的 watch,代码会变得冗余且难以维护。而用一个 watch 同时监听这两个值,逻辑就清晰多了。

2. 数据筛选与搜索

这是最经典的应用场景。一个商品列表页面,筛选条件可能包括:

  • 关键词
  • 价格范围
  • 品牌
  • 分类
  • 是否有货

这些条件通常是独立的 refreactive 对象的属性。用户修改其中任何一个,我们都应该重新调用后端接口获取最新的商品列表。用一个 watch 来统一管理这些筛选条件,是最高效、最直接的方式。

3. 依赖多个状态的 UI 逻辑

有时候,一个 UI 元素的显示与否,取决于多个状态的组合。例如,一个"提交"按钮,我们希望它满足以下条件时才可点击:

  • 表单数据已修改(isFormDirtytrue
  • 当前没有正在进行的提交操作(isSubmittingfalse
  • 所有必填字段都已验证通过(isFormValidtrue

这个按钮的 disabled 状态,就依赖于 isFormDirtyisSubmittingisFormValid 这三个布尔值。我们可以用一个 watch 来监听这三个值的变化,然后计算出最终的 disabled 状态。

4. 游戏或复杂交互中的状态同步

在一些富交互应用,比如在线小游戏或者数据可视化大屏中,一个动作可能会触发多个状态的变化,而这些状态的变化又可能需要协同触发一些动画、音效或数据更新。例如,在游戏中,玩家的"生命值"和"魔法值"同时变化时,可能需要同时更新 UI 上的两个数值条,并触发一个"暴走"模式的判断。这种场景下,监听多个数据源是必不可少的。

通过这些场景,你应该能感受到,监听多个数据源并非一个可有可无的"高级技巧",而是处理真实世界复杂业务逻辑的刚需。它能让我们的代码更加内聚、逻辑更加清晰,是每一位 Vue 开发者都应该熟练掌握的利器。


二、 watch 的核心基础:温故而知新

在直接冲向"监听多个数据源"这个核心目标之前,我们先稳扎稳打,回顾一下 watch 的基本用法。这就像练武功,先要扎好马步。这里,我们还会将它与它的"兄弟"------watchEffect------进行对比,因为理解它们的异同,对于在正确场景选择正确工具至关重要。

2.1 watch 的本质:一个精确的观察者

watch API 的本质是什么?你可以把它想象成一个非常专注且有耐心的观察者。你明确地告诉它:"嘿,伙计,请你帮我盯着这个叫 source 的东西。只有当 source 的值发生变化时,你才需要执行这个 callback 函数。如果 source 没变,你啥也不用干,就在那儿歇着。"

这种"明确指定"的特性,是 watch 最核心的标签。它需要两个最基本的参数:

  1. 数据源 :你想监听的那个响应式数据。它可以是一个 ref,一个 reactive 对象,或者是一个返回值的 getter 函数。
  2. 回调函数:当数据源变化时,要执行的逻辑。

它的基本签名是这样的:

javascript 复制代码
watch(source, callback, options?)

我们来看一个最简单的例子,监听一个 ref

vue 复制代码
<script setup>
import { ref, watch } from 'vue';

// 定义一个响应式数据源,一个计数器
const count = ref(0);

// 使用 watch 来监听 count 的变化
watch(count, (newValue, oldValue) => {
  // 这个回调函数会在 count.value 变化后执行
  console.log(`count 发生了变化!新值是: ${newValue}, 旧值是: ${oldValue}`);
});

// 模拟用户操作,2秒后让 count 加 1
setTimeout(() => {
  count.value++; // 这会触发上面的 watch 回调
}, 2000);
</script>

<template>
  <div>当前计数: {{ count }}</div>
</template>

在这个例子中:

  • source 就是 count 这个 ref 对象。
  • callback 就是我们传入的箭头函数 (newValue, oldValue) => { ... }
  • Vue 会自动把变化后的新值 newValue 和变化前的旧值 oldValue 传递给我们的回调函数,非常贴心。
2.2 watchEffect:一个自动追踪的"懒人"观察者

现在,我们来看看 watch 的兄弟------watchEffect。如果说 watch 是一个需要你明确指定目标的"精确射手",那么 watchEffect 就像一个"自动追踪的侦探"。

你不需要告诉 watchEffect 要监听谁。你只需要把要执行的副作用逻辑写在一个回调函数里传给它。watchEffect 会自动地在函数执行过程中,分析哪些响应式数据被"访问"了,然后它就会默默地把这些数据都记录下来,作为自己的监听目标。

vue 复制代码
<script setup>
import { ref, watchEffect } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

// watchEffect 会自动追踪回调函数内使用的响应式数据
watchEffect(() => {
  // 在这个函数里,我们访问了 firstName.value 和 lastName.value
  // watchEffect 就会自动监听这两个 ref
  console.log(`全名是: ${firstName.value}${lastName.value}`);
});

// 2秒后改变姓氏
setTimeout(() => {
  firstName.value = '李'; // 这会触发 watchEffect 的重新执行
}, 2000);

// 4秒后改变名字
setTimeout(() => {
  lastName.value = '四'; // 这同样会触发 watchEffect 的重新执行
}, 4000);
</script>

<template>
  <div>
    <input v-model="firstName" placeholder="姓" />
    <input v-model="lastName" placeholder="名" />
  </div>
</template>

看到了吗?我们没有显式地告诉 watchEffect 去监听 firstNamelastName,但它做到了。这就是它的"懒人"之处。

2.3 watch vs watchEffect:一张表格看懂核心区别

为了让你更清晰地理解它们的差异,我准备了一张对比表格。这比长篇大论的文字描述要直观得多。

特性 watch watchEffect
追踪方式 显式指定。你需要明确告诉它要监听哪个或哪些数据源。 自动隐式追踪。它会自动收集回调函数内部访问到的所有响应式数据。
回调触发时机 惰性 。默认情况下,只有在监听的数据源发生变化后才会执行回调。 立即执行 。在组件初始化时,会立即执行一次回调函数,以建立依赖关系。
获取新旧值 可以 。回调函数会接收 newValueoldValue 两个参数。 不可以。它不关心具体是哪个值变了,只关心依赖变化后需要重新执行副作用,所以没有新旧值的概念。
访问时机 在回调函数中,可以访问到变化之后 的 DOM(默认 flush: 'pre',在 DOM 更新前)。 同样可以访问到变化后的 DOM(默认 flush: 'post',在 DOM 更新后,但 Vue 3.2+ 默认也是 'pre')。
适用场景 1. 需要在数据变化时执行异步或开销较大 的操作。 2. 需要知道具体是哪个值变了 ,以及变化前后的值。 3. 需要监听特定的、不常变化的数据源。 1. 当副作用逻辑依赖多个数据源 ,且不想显式地一一列出时。 2. 不需要关心旧值,只想在依赖变化时重新运行 某些逻辑。 3. 逻辑和依赖关系非常紧密,写在一起更直观。

通俗解读一下:

  • watch:就像你设定了一个闹钟,"每天早上 7 点响"。你明确指定了时间(数据源),到了时间就执行"起床"(回调)。你很清楚闹钟为什么会响。
  • watchEffect:就像你对你的智能音箱说,"如果天黑了或者下雨了,就把客厅的灯打开"。你不需要告诉它具体去监听"光照传感器"和"雨水传感器",它会自己搞定。你只关心结果(灯打开),不关心是哪个条件触发的。

在监听多个数据源这个主题下,watchwatchEffect 都能实现我们的目标,但它们的方式和适用场景有所不同。接下来,我们将重点聚焦于 watch,因为它提供了更精细的控制,尤其是在处理复杂逻辑时。


三、 核心技法:watch 如何监听多个数据源

好了,铺垫了这么多,终于来到了我们的核心环节。Vue 3 的 watch API 设计得非常优雅,监听多个数据源的方式也相当直观。最核心、最常用的方法就是:将多个数据源放在一个数组里

3.1 基础语法:数组作为第一个参数

这是最标准、最官方的做法。当你想把 watch 的"监控目标"从一个扩展到多个时,你只需要把原本传给 watch 的第一个参数从单个数据源,改成包含多个数据源的数组即可。

语法结构如下:

javascript 复制代码
watch(
  [source1, source2, source3, ...], // <-- 关键点:数据源数组
  (newValues, oldValues) => {
    // 副作用回调函数
  },
  options? // 可选的配置项
)

重点来了:回调函数的参数变化。

当监听单个数据源时,回调函数接收 (newValue, oldValue)。那么,当监听一个数组的数据源时,回调函数的参数会变成什么样呢?

答案是:回调函数接收的两个参数(新值数组和旧值数组)与数据源数组一一对应。

  • newValues:一个数组,包含了所有数据源变化后的新值,顺序与你在 watch 第一个参数中定义的顺序完全一致。
  • oldValues:同样是一个数组,包含了所有数据源变化前的旧值,顺序也完全一致。

让我们用一个生动的例子来实践一下。假设我们正在开发一个图形绘制工具,用户可以通过两个滑块分别控制一个圆形的 xy 坐标。我们希望在坐标更新时,打印出圆形移动的轨迹。

vue 复制代码
<script setup>
import { ref, watch } from 'vue';

// 定义两个独立的 ref,分别代表 x 和 y 坐标
const circleX = ref(50);
const circleY = ref(50);

// 使用数组语法同时监听 circleX 和 circleY
watch(
  [circleX, circleY], // <-- 将两个 ref 放入数组
  (newValues, oldValues) => {
    // newValues 和 oldValues 都是数组
    const [newX, newY] = newValues; // 解构获取新值
    const [oldX, oldY] = oldValues; // 解构获取旧值

    console.log('圆形坐标发生了变化!');
    console.log(`从 (${oldX}, ${oldY}) 移动到了 (${newX}, ${newY})`);
  }
);

// 为了演示,我们模拟一些用户操作
setTimeout(() => {
  circleX.value = 100; // 只改变 x
}, 1000);

setTimeout(() => {
  circleY.value = 120; // 只改变 y
}, 2500);

setTimeout(() => {
  circleX.value = 150;
  circleY.value = 80; // 同时改变 x 和 y
}, 4000);
</script>

<template>
  <div class="canvas-container">
    <div class="circle" :style="{ left: circleX + 'px', top: circleY + 'px' }"></div>
    <div class="controls">
      <label>
        X 坐标: {{ circleX }}
        <input type="range" v-model="circleX" min="0" max="200" />
      </label>
      <label>
        Y 坐标: {{ circleY }}
        <input type="range" v-model="circleY" min="0" max="200" />
      </label>
    </div>
  </div>
</template>

<style>
.canvas-container {
  position: relative;
  width: 250px;
  height: 250px;
  border: 1px solid #ccc;
}
.circle {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: dodgerblue;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
.controls {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
</style>

代码剖析:

  1. [circleX, circleY] :这是核心。我们告诉 watch:"请同时关注 circleXcircleY 这两个'信号'。"
  2. (newValues, oldValues) => { ... } :当 circleXcircleY 中任何一个发生变化时,这个回调就会被触发。
  3. const [newX, newY] = newValues; :我们使用数组的解构赋值,非常方便地从新值数组中取出对应位置的值。newValues[0] 对应 circleX 的新值,newValues[1] 对应 circleY 的新值。oldValues 同理。
  4. 运行结果
    • 1秒后,circleX 变为 100,控制台会打印:从 (50, 50) 移动到了 (100, 50)。注意,oldValuescircleY 的值仍然是 50,因为它这次没变。
    • 2.5秒后,circleY 变为 120,控制台会打印:从 (100, 50) 移动到了 (100, 120)
    • 4秒后,两者同时改变,控制台会打印:从 (100, 120) 移动到了 (150, 80)

这个例子完美地展示了 watch 监听多个数据源的基本流程和参数结构。是不是感觉很简单?是的,Vue 的设计哲学就是这样,把复杂的事情用简单直观的 API 暴露给开发者。

3.2 监听 reactive 对象的多个属性

在 Vue 3 中,我们除了使用 ref,还经常使用 reactive 来创建响应式对象。那么,如何监听一个 reactive 对象内部的多个属性呢?

假设我们有一个用户信息对象:

javascript 复制代码
const userInfo = reactive({
  name: '张三',
  age: 25,
  address: '北京市'
});

如果我们想同时监听 nameage 的变化,有几种方法。

方法一:使用 getter 函数数组(推荐)

这是最精确、性能最好的方法。我们不需要监听整个 userInfo 对象,而是只关心它内部的特定属性。这可以通过提供返回这些属性的 getter 函数来实现。

vue 复制代码
<script setup>
import { reactive, watch } from 'vue';

const userInfo = reactive({
  name: '张三',
  age: 25,
  address: '北京市'
});

// 使用 getter 函数数组来精确监听
watch(
  [
    () => userInfo.name, // getter 1:返回 name 属性
    () => userInfo.age   // getter 2:返回 age 属性
  ],
  (newValues, oldValues) => {
    const [newName, newAge] = newValues;
    const [oldName, oldAge] = oldValues;
    console.log(`用户信息更新:姓名从 ${oldName} 变为 ${newName},年龄从 ${oldAge} 变为 ${newAge}`);
  }
);

// 模拟数据变化
setTimeout(() => {
  userInfo.name = '李四'; // 触发 watch
}, 2000);

setTimeout(() => {
  userInfo.age = 26; // 触发 watch
}, 4000);

setTimeout(() => {
  userInfo.address = '上海市'; // 不会触发 watch,因为我们没监听它
}, 6000);
</script>

为什么这种方法好?

  • 精确 :它告诉 Vue:"我只想知道 userInfo.nameuserInfo.age 的变化,其他的我不管。"
  • 高效 :Vue 不需要去深度遍历整个 userInfo 对象,只需要检查这两个特定的属性。当对象很大很复杂时,这种性能优势会非常明显。
  • 清晰 :代码意图一目了然,其他开发者能立刻看出这个 watch 关心的是哪些具体属性。

方法二:直接监听整个 reactive 对象(配合 deep: true

我们也可以尝试直接监听 userInfo 这个对象本身。

vue 复制代码
<script setup>
import { reactive, watch } from 'vue';

const userInfo = reactive({
  name: '张三',
  age: 25,
  details: {
    hobby: ' coding'
  }
});

// 直接监听整个 reactive 对象
watch(
  userInfo, // <-- 直接传入 reactive 对象
  (newValue, oldValue) => {
    // 注意:对于 reactive 对象,newValue 和 oldValue 是同一个对象引用!
    // 因为对象本身没有变,只是其内部的属性变了
    console.log('userInfo 对象发生了变化');
    console.log('新值:', newValue);
    console.log('旧值:', oldValue); // 会发现和 newValue 是完全相等的
    console.log('新旧值是否相等:', newValue === oldValue); // true
  },
  { deep: true } // <-- 关键!必须开启深度监听
);

// 模拟数据变化
setTimeout(() => {
  userInfo.name = '王五'; // 触发 watch
}, 2000);

setTimeout(() => {
  userInfo.details.hobby = 'swimming'; // 也能触发 watch,因为 deep: true
}, 4000);
</script>

这种方法的注意事项:

  1. 必须加 deep: true :默认情况下,watch 监听对象是浅层的。也就是说,只有当你把整个 userInfo 替换成一个新对象时,它才会触发。而修改 userInfo.name 这种内部属性的变化,是监听不到的。deep: true 就是告诉 watch:"请你深入到对象内部,检查所有嵌套属性的变化。"
  2. newValueoldValue 相等 :这是一个非常重要的知识点!当你监听一个 reactive 对象时,watch 回调中的 newValueoldValue 指向的是同一个对象引用 。因为对象本身没有被替换,只是它的内容被修改了。所以,你无法通过 newValue === oldValue 来判断对象是否发生了变化(它们永远是 true),也无法通过这种方式获取到变化前的"快照"。
  3. 性能开销deep: true 会递归地遍历对象的所有属性,如果对象结构非常深或非常庞大,这会带来一定的性能开销。

对比与选择:

方法 优点 缺点 适用场景
Getter 函数数组 精确、高效、清晰、能获取新旧值 需要为每个属性写一个 getter 强烈推荐。当你只关心对象中的少数几个属性时。
监听整个对象 + deep: true 简单(不需要写多个 getter) 性能开销大、无法获取真正的旧值、无法精确控制 当对象结构不深,且你需要响应对象内任意属性的变化时。

在绝大多数情况下,使用 getter 函数数组是更优的选择。它体现了"按需监听"的原则,让你的代码更健壮、更高效。

3.3 混搭监听:refreactive Getter 一起上

Vue 3 的响应式系统非常灵活,watch 也继承了这一优点。在监听多个数据源时,你完全不必拘泥于数据源的类型。可以自由地将 refreactive 对象的 getter 函数,甚至是另一个 computed 的值,混合放在同一个数组里。

这种"混搭"的能力,使得 watch 能够适应任何复杂的业务场景。

假设我们正在开发一个在线考试系统,考试的提交状态取决于:

  1. 所有题目是否都已作答(一个 computed 值)。
  2. 考试的剩余时间(一个 ref)。
  3. 考试配置中的"自动提交"开关是否打开(一个 reactive 对象的属性)。
vue 复制代码
<script setup>
import { ref, reactive, computed, watch } from 'vue';

// 1. 题目作答状态
const answers = reactive({
  q1: '',
  q2: '',
  q3: ''
});

// 计算属性:是否所有题目都已作答
const allQuestionsAnswered = computed(() => {
  return Object.values(answers).every(answer => answer !== '');
});

// 2. 考试剩余时间(秒)
const remainingTime = ref(3600); // 1小时

// 3. 考试配置
const examConfig = reactive({
  autoSubmit: true,
  allowReview: false
});

// 混搭监听!
watch(
  [
    allQuestionsAnswered,      // <-- 一个 computed
    remainingTime,             // <-- 一个 ref
    () => examConfig.autoSubmit // <-- 一个 reactive 对象的 getter
  ],
  (newValues) => {
    const [areAllAnswered, timeLeft, isAutoSubmitOn] = newValues;
    
    console.log('--- 状态检查 ---');
    console.log(`所有题目已作答: ${areAllAnswered}`);
    console.log(`剩余时间: ${timeLeft}秒`);
    console.log(`自动提交开关: ${isAutoSubmitOn}`);

    // 业务逻辑:如果满足条件,则自动提交
    if (areAllAnswered && isAutoSubmitOn) {
      console.log('条件满足,准备自动提交试卷!');
      // submitExam();
    }

    // 业务逻辑:如果时间耗尽,也强制提交
    if (timeLeft <= 0) {
      console.log('时间到!强制提交试卷!');
      // submitExam();
    }
  }
);

// 模拟用户操作
setTimeout(() => {
  answers.q1 = 'A';
}, 1000);

setTimeout(() => {
  answers.q2 = 'B';
}, 2000);

setTimeout(() => {
  answers.q3 = 'C'; // 此时 allQuestionsAnswered 会变为 true,触发 watch
}, 3000);

// 模拟时间流逝
setInterval(() => {
  if (remainingTime.value > 0) {
    remainingTime.value--;
  }
}, 1000);
</script>

<template>
  <div>
    <h2>在线考试系统</h2>
    <p>所有题目已作答: {{ allQuestionsAnswered ? '是' : '否' }}</p>
    <p>剩余时间: {{ Math.floor(remainingTime.value / 60) }}分{{ remainingTime.value % 60 }}秒</p>
    <p>自动提交: {{ examConfig.autoSubmit ? '开启' : '关闭' }}</p>
    <hr>
    <div>
      <p>问题1: <input v-model="answers.q1" /></p>
      <p>问题2: <input v-model="answers.q2" /></p>
      <p>问题3: <input v-model="answers.q3" /></p>
    </div>
  </div>
</template>

代码解读:

  • [allQuestionsAnswered, remainingTime, () => examConfig.autoSubmit]:这个数组就是我们的"监控面板",上面挂载了三种不同类型的"监控设备"。
  • watch 回调 :它不关心数据源来自哪里,只关心它们变化后的值。newValues 数组会按顺序提供这些最新的值。
  • 逻辑解耦:我们将复杂的提交逻辑,与多个零散的状态变化关联了起来。任何一方状态的变化,都会触发一次完整的"可提交性"检查,使得整个系统的状态管理非常稳健。

这个例子充分展示了 watch 的强大和灵活性。它就像一个万能的适配器,可以接入各种类型的响应式信号,并将它们统一处理。


四、 高阶玩法:watch 的配置项与生命周期

掌握了基本的监听方法后,我们来看看那些能让 watch 变得更加强大和精细的"高级开关"。watch 的第三个参数是一个可选的配置对象,通过它,我们可以控制 watch 的行为,比如是否立即执行、是否深度监听、以及回调的执行时机等。

4.1 配置项详解:deep, immediate, flush

这个配置对象通常长这样:{ deep: boolean, immediate: boolean, flush: string }。我们来逐一拆解。

4.1.1 deep: true ------ 深入敌后

我们前面已经提到过 deep: true,这里再系统地总结一下。

  • 作用 :当监听的数据源是一个对象或数组时,强制 watch 深入其内部,监听其嵌套属性或元素的变化。
  • 默认值false
  • 何时使用
    • 当你直接监听一个 reactive 对象,并希望其内部任何属性的变化都能触发回调时。
    • 当你监听一个 ref,但这个 ref 的值是一个复杂对象,你希望监听这个对象内部的变化时。

示例:监听 ref 包含的对象

javascript 复制代码
const state = ref({
  count: 0,
  nested: {
    value: 'a'
  }
});

// 默认情况下,这样监听不到 nested.value 的变化
watch(state, () => {
  console.log('state 变化了');
}); // state.value.nested.value = 'b' 不会触发

// 开启 deep 监听
watch(state, () => {
  console.log('state 深度变化了');
}, { deep: true }); // state.value.nested.value = 'b' 会触发
  • 性能警告deep: true 很方便,但代价是性能。它会递归遍历整个数据结构。如果你的对象非常庞大,或者监听频率非常高,这可能会成为性能瓶颈。因此,优先考虑使用 getter 函数数组来精确监听 ,只有在必要时才使用 deep: true
4.1.2 immediate: true ------ 立即执行
  • 作用 :在 watch 被创建时,立即执行一次回调函数,而不用等到第一次数据变化。
  • 默认值false
  • 何时使用
    • 当你需要在组件初始化时,就根据数据源的初始值执行一次副作用操作时。
    • 一个非常常见的场景:根据初始的查询参数,立即加载一次数据。

示例:初始化数据加载

vue 复制代码
<script setup>
import { ref, watch } from 'vue';

// 模拟从路由或父组件传来的查询参数
const queryParams = ref({ categoryId: 101, keyword: '手机' });

const productList = ref([]);
const isLoading = ref(false);

const fetchProducts = async (params) => {
  isLoading.value = true;
  console.log(`正在根据参数 ${JSON.stringify(params)} 获取商品列表...`);
  // 模拟 API 请求
  await new Promise(resolve => setTimeout(resolve, 1000));
  productList.value = [`商品1 (${params.keyword})`, `商品2 (${params.keyword})`];
  isLoading.value = false;
};

// 使用 immediate: true,在组件创建时立即加载数据
watch(
  queryParams,
  (newParams) => {
    fetchProducts(newParams);
  },
  { immediate: true } // <-- 关键!
);

// 3秒后,用户修改了筛选条件
setTimeout(() => {
  queryParams.value.keyword = '电脑';
}, 3000);
</script>

<template>
  <div>
    <p v-if="isLoading">加载中...</p>
    <ul v-else>
      <li v-for="product in productList" :key="product">{{ product }}</li>
    </ul>
  </div>
</template>

在这个例子中,如果没有 immediate: true,那么页面刚加载时,商品列表是空的,只有当 queryParams 变化时才会去加载。而加上 immediate: true 后,fetchProducts 会在 watch 建立时就立刻执行一次,完美地实现了"初始化加载"的需求。

4.1.3 flush: 'pre' | 'post' | 'sync' ------ 精确控制执行时机

这是 watch 最微妙也最强大的配置项,它控制着回调函数在 Vue 的更新周期中执行的时机。理解它需要你对 Vue 的渲染流程有一个基本的认识。

Vue 的组件更新流程可以简化为:

  1. 响应式数据变化
  2. 触发副作用 (如 watch 回调)。
  3. 重新渲染模板(更新 DOM)。
  4. 挂载更新后的 DOM

flush 就是用来决定第 2 步在何时发生的。

  • flush: 'pre' (默认值)

    • 含义'pre' 代表 "pre-flush",即在组件更新之前执行回调。
    • 时机 :数据变化 -> watch 回调执行 -> DOM 更新。
    • 特点 :在回调中,你访问到的是更新前 的 DOM。但你可以访问到数据变化后的最新值
    • 适用场景:大多数情况下,这是最佳选择。因为如果你在回调中修改了另一个响应式数据,Vue 可以将这个新的变化也合并到同一次更新中,避免不必要的重复渲染。
  • flush: 'post'

    • 含义'post' 代表 "post-flush",即在组件更新之后执行回调。
    • 时机 :数据变化 -> DOM 更新 -> watch 回调执行。
    • 特点 :在回调中,你可以访问到更新后的 DOM。
    • 适用场景 :当你的副作用逻辑需要操作或依赖于更新后的 DOM 元素时。例如,你需要在一个 v-for 列表更新后,获取其中某个元素的新尺寸或位置。
  • flush: 'sync'

    • 含义'sync' 代表同步执行。
    • 时机 :数据变化 -> 立即同步 执行 watch 回调 -> (然后才可能触发)DOM 更新。
    • 特点:它的执行效率最低,且容易导致性能问题和无限循环。因为它会打断 Vue 的正常批量更新机制。
    • 适用场景极少数情况 。比如,你需要确保在一个特定数据变化后,另一个数据在同一事件循环中 被立即更新,并且这个操作不能被延迟。官方文档强烈建议谨慎使用,除非你非常清楚你在做什么。

flush 时机对比图

为了更直观地理解,我用 Mermaid 画一个流程图来对比这三种模式:
'pre' (默认) 'post' 'sync' 是 否 响应式数据变化 flush 配置? 执行 watch 回调 Vue 更新 DOM 视图渲染完成 Vue 更新 DOM 执行 watch 回调 视图渲染完成 立即同步执行 watch 回调 回调中是否修改了其他数据? 可能触发更多同步更新 等待下一个 tick 更新 DOM 视图渲染完成

示例:flush: 'post' 的实际应用

假设我们有一个可调整大小的 div,我们想在它大小变化后,获取它新的宽度。

vue 复制代码
<script setup>
import { ref, watch, nextTick } from 'vue';

const boxRef = ref(null);
const boxWidth = ref(100);

// 我们希望在 DOM 更新后,获取元素的实际宽度
watch(
  boxWidth,
  async () => {
    // 使用 flush: 'post',确保 DOM 已经更新
    console.log('watch 回调执行 (flush: post)');
    // 此时可以安全地访问更新后的 DOM
    if (boxRef.value) {
      console.log('DOM 中的宽度是:', boxRef.value.offsetWidth);
    }
  },
  { flush: 'post' } // <-- 关键
);

// 对比一下,如果不使用 'post'
watch(
  boxWidth,
  () => {
    console.log('watch 回调执行 (flush: pre, 默认)');
    // 此时 DOM 还是旧的
    if (boxRef.value) {
      console.log('DOM 中的宽度是:', boxRef.value.offsetWidth);
    }
  }
);

const changeWidth = () => {
  boxWidth.value = 200;
};
</script>

<template>
  <div>
    <button @click="changeWidth">改变宽度</button>
    <div
      ref="boxRef"
      :style="{ width: boxWidth + 'px', height: '50px', backgroundColor: 'lightblue' }"
    >
      我是一个盒子
    </div>
  </div>
</template>

当你点击按钮时,控制台会输出:

复制代码
watch 回调执行 (flush: pre, 默认)
DOM 中的宽度是: 100
watch 回调执行 (flush: post)
DOM 中的宽度是: 200

这个结果清晰地展示了 'pre''post' 的区别。'post' 让我们拿到了更新后的 DOM 尺寸,这在很多需要与 DOM 交互的库(如 D3.js、一些动画库)中非常有用。

4.2 副作用的清理:onCleanup 函数

watch 的世界里,副作用并不仅仅是执行一个函数。很多时候,这个副作用会"留下一些痕迹",比如开启了一个定时器、发起了一个网络请求、或者建立了一个 WebSocket 连接。如果组件被销毁了,或者数据源再次变化导致副作用要重新执行,这些"痕迹"就需要被清理掉,否则就会造成内存泄漏。

Vue 3 的 watch 提供了一个非常优雅的清理机制:onCleanup 函数

onCleanup 是一个可以被注册在 watch 回调内部的函数。你只需要把清理逻辑放在一个函数里,然后把这个函数传给 onCleanup。Vue 会在以下时机自动执行这个清理函数:

  1. 副作用即将重新执行之前(即数据源再次变化时)。
  2. watch 所在的组件被卸载时

经典场景:防抖搜索与请求取消

这是一个非常经典的例子。我们有一个搜索框,用户输入时,我们不希望每次按键都立即发请求,而是等用户停止输入一小段时间(比如 500ms)后再发。同时,如果用户在 500ms 内又输入了新的内容,那么上一次还没发出去的请求就应该被取消。

vue 复制代码
<script setup>
import { ref, watch, onUnmounted } from 'vue';

const searchQuery = ref('');
let debounceTimer = null;
let abortController = null; // 用于取消 fetch 请求

watch(
  searchQuery,
  (newQuery, oldQuery, onCleanup) => { // <-- onCleanup 是第三个参数
    console.log(`搜索词从 "${oldQuery}" 变为 "${newQuery}"`);

    // 1. 清理上一次的副作用
    // 如果存在上一次的定时器,先清除掉
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    // 如果存在上一次的请求控制器,先取消请求
    if (abortController) {
      abortController.abort();
    }

    // 2. 注册本次的清理函数
    // onCleanup 会在下一次 watch 触发或组件卸载时执行
    onCleanup(() => {
      console.log(`执行清理:取消搜索 "${newQuery}" 的请求`);
      if (abortController) {
        abortController.abort();
      }
    });

    // 3. 设置新的防抖定时器
    debounceTimer = setTimeout(async () => {
      console.log(`准备发起搜索请求: "${newQuery}"`);
      if (!newQuery) {
        console.log('搜索词为空,不请求');
        return;
      }

      // 为本次请求创建一个新的 AbortController
      abortController = new AbortController();
      const { signal } = abortController;

      try {
        // 模拟 API 请求,并传入 signal
        const response = await fetch(`https://api.example.com/search?q=${newQuery}`, { signal });
        const data = await response.json();
        console.log('请求成功:', data);
      } catch (error) {
        // 如果请求被取消,error.name 会是 'AbortError'
        if (error.name !== 'AbortError') {
          console.error('请求失败:', error);
        }
      }
    }, 500); // 500ms 防抖
  }
);

// 组件卸载时,确保最后的清理工作也完成
onUnmounted(() => {
    if (debounceTimer) {
        clearTimeout(debounceTimer);
    }
    if (abortController) {
        abortController.abort();
    }
});
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="输入搜索词..." />
  </div>
</template>

代码深度剖析:

  1. onCleanup 的引入 :注意 watch 回调的签名,它接收第三个参数 onCleanup
  2. 清理逻辑前置:在设置新的副作用(开启新的定时器)之前,我们先执行清理逻辑(清除旧的定时器,取消旧的请求)。这是一个好习惯。
  3. 注册清理函数onCleanup(() => { ... }) 这行代码是核心。我们告诉 Vue:"如果下次要重新执行这个 watch,或者我要挂了,请务必帮我执行这个清理函数。"
  4. AbortController :这是现代浏览器提供的用于取消 fetch 请求的标准 API。我们为每次请求都创建一个新的 controller,并把它的 signal 传给 fetch。当调用 controller.abort() 时,对应的 fetch 请求就会被中断。
  5. onUnmounted :虽然 onCleanup 在组件卸载时也会被调用,但为了代码的健壮性和清晰性,在 onUnmounted 中再显式地执行一次清理,确保万无一失。

通过 onCleanup,Vue 为我们提供了一种声明式的、可预测的方式来管理副作用的"生命周期",让我们的代码更加健壮,避免了内存泄漏这个前端开发中的常见"大坑"。

4.3 手动停止监听:stopWatch 函数

通常情况下,watch 的生命周期与组件绑定。组件创建,watch 开始工作;组件销毁,watch 自动停止。但有时候,我们可能需要在组件仍然存活的时候,手动地、提前地停止一个 watch

watch API 在被调用时,会返回一个函数,调用这个函数,就可以手动停止对应的 watch

语法:

javascript 复制代码
const stopWatcher = watch(source, callback);

// 在某个时刻,调用这个函数来停止监听
stopWatcher();

应用场景:

假设我们有一个组件,它根据一个 isActive 的 prop 来决定是否开启某个监听。当 isActive 变为 false 时,我们希望停止这个监听以节省资源。

vue 复制代码
<script setup>
import { ref, watch, onUnmounted } from 'vue';

const props = defineProps({
  isActive: {
    type: Boolean,
    default: true
  }
});

const sourceData = ref(0);

let stopWatcher = null;

const startWatching = () => {
  if (stopWatcher) {
    console.log('监听器已存在,无需重复创建。');
    return;
  }
  console.log('开始监听 sourceData...');
  stopWatcher = watch(
    sourceData,
    () => {
      console.log(`sourceData 变化了: ${sourceData.value}`);
    }
  );
};

const stopWatching = () => {
  if (stopWatcher) {
    console.log('手动停止监听 sourceData。');
    stopWatcher(); // <-- 调用返回的函数来停止
    stopWatcher = null; // 将引用置空
  } else {
    console.log('监听器不存在,无需停止。');
  }
};

// 根据 prop 的变化来启动或停止监听
watch(
  () => props.isActive,
  (isActive) => {
    if (isActive) {
      startWatching();
    } else {
      stopWatching();
    }
  },
  { immediate: true } // 组件初始化时就根据 isActive 的值来决定
);

// 模拟数据变化
setInterval(() => {
  sourceData.value++;
}, 2000);
</script>

<template>
  <div>
    <p>当前监听状态: {{ props.isActive ? '开启' : '关闭' }}</p>
    <p>sourceData: {{ sourceData }}</p>
    <button @click="stopWatching">手动停止监听</button>
    <button @click="startWatching">手动开启监听</button>
  </div>
</template>

在这个例子中,我们通过 stopWatcher() 函数,实现了对 watch 生命周期的完全手动控制。这在一些需要精细化管理资源和副作用的复杂组件中非常有用。


五、 实战应用场景与模式

理论学了一大堆,是时候把它们放到真实的"战场"上看看了。下面我将列举几个在开发中非常常见的应用场景,并展示如何运用我们学到的 watch 知识来优雅地解决问题。

5.1 场景一:实时表单验证与联动

这是 watch 监听多个数据源最经典、最直接的应用。一个复杂的表单,往往一个字段的校验依赖于其他字段。

需求:注册表单,包含"密码"和"确认密码"。要求:

  1. 密码长度至少 8 位。
  2. 确认密码必须与密码一致。
  3. 只有当所有条件都满足时,"注册"按钮才可点击。
vue 复制代码
<script setup>
import { ref, watch } from 'vue';

const password = ref('');
const confirmPassword = ref('');
const isFormValid = ref(false); // 表单整体是否有效
const passwordError = ref('');
const confirmPasswordError = ref('');

// 核心逻辑:同时监听密码和确认密码
watch(
  [password, confirmPassword],
  ([newPass, newConfirmPass]) => {
    console.log('表单数据变化,开始验证...');

    // 1. 验证密码
    if (newPass.length < 8) {
      passwordError.value = '密码长度不能少于8位';
    } else {
      passwordError.value = '';
    }

    // 2. 验证确认密码
    if (newConfirmPass && newPass !== newConfirmPass) {
      confirmPasswordError.value = '两次输入的密码不一致';
    } else {
      confirmPasswordError.value = '';
    }

    // 3. 计算表单整体有效性
    isFormValid.value = newPass.length >= 8 && newPass === newConfirmPass;
  }
);
</script>

<template>
  <form>
    <div>
      <label>密码:</label>
      <input type="password" v-model="password" />
      <span class="error">{{ passwordError }}</span>
    </div>
    <div>
      <label>确认密码:</label>
      <input type="password" v-model="confirmPassword" />
      <span class="error">{{ confirmPasswordError }}</span>
    </div>
    <button type="submit" :disabled="!isFormValid">
      注册
    </button>
  </form>
</template>

<style>
.error {
  color: red;
  margin-left: 10px;
}
button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

模式解读:

这个例子完美地展示了 watch 如何将多个分散的状态(password, confirmPassword)聚合起来,驱动一个更复杂的状态(isFormValid)和多个派生状态(passwordError, confirmPasswordError)。所有的校验逻辑都集中在 watch 回调中,使得代码非常内聚,易于维护。

5.2 场景二:带防抖的智能搜索

我们前面讲 onCleanup 时已经接触过这个场景,这里我们把它提炼成一个可复用的模式。

需求:一个搜索输入框,用户输入后,等待 500ms 无新输入才发起搜索,且需要取消前一次未完成的请求。

这个模式的核心是结合 watchsetTimeoutAbortController

javascript 复制代码
// 这是一个可以封装成 Composable 的逻辑
import { ref, watch } from 'vue';

export function useDebouncedSearch(apiCallback) {
  const searchQuery = ref('');
  const results = ref([]);
  const isLoading = ref(false);
  const error = ref(null);

  let abortController = null;

  watch(
    searchQuery,
    (newQuery, oldQuery, onCleanup) => {
      // 清理
      if (abortController) {
        abortController.abort();
      }
      // 注册清理
      onCleanup(() => {
        if (abortController) {
          abortController.abort();
        }
      });

      if (!newQuery.trim()) {
        results.value = [];
        return;
      }

      isLoading.value = true;
      error.value = null;
      
      abortController = new AbortController();
      const { signal } = abortController;

      // 防抖
      const timer = setTimeout(async () => {
        try {
          results.value = await apiCallback(newQuery, { signal });
        } catch (e) {
          if (e.name !== 'AbortError') {
            error.value = e;
          }
        } finally {
          isLoading.value = false;
        }
      }, 500);

      // 也可以在 onCleanup 里清除 timer
      onCleanup(() => clearTimeout(timer));
    }
  );

  return {
    searchQuery,
    results,
    isLoading,
    error
  };
}

这个 useDebouncedSearch 函数(一个 Vue Composable)封装了所有复杂的逻辑。在任何组件中,你只需要这样使用:

vue 复制代码
<script setup>
import { useDebouncedSearch } from './useDebouncedSearch';

// 假设这是你的 API 函数
const fakeSearchAPI = async (query, { signal }) => {
  console.log(`Searching for ${query}...`);
  await new Promise(res => setTimeout(res, 1000));
  return [`Result 1 for ${query}`, `Result 2 for ${query}`];
};

const { searchQuery, results, isLoading, error } = useDebouncedSearch(fakeSearchAPI);
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Search..." />
    <div v-if="isLoading">Loading...</div>
    <div v-if="error">Error: {{ error.message }}</div>
    <ul>
      <li v-for="result in results" :key="result">{{ result }}</li>
    </ul>
  </div>
</template>

模式解读:

这个模式展示了如何将 watch 与外部 API(fetch)和浏览器 API(setTimeout, AbortController)深度结合,构建出健壮且用户体验良好的功能。将逻辑封装成 Composable 是 Vue 3 推崇的最佳实践,它极大地提高了代码的复用性和可测试性。

5.3 场景三:数据分页与筛选器联动

在后台管理系统中,列表页是家常便饭。这类页面的核心就是处理分页、排序、筛选等多个参数的联动。

需求:一个用户列表,支持按姓名搜索,按部门筛选,以及分页。任何一个参数变化,都需要重置页码到第一页,并重新请求数据。

vue 复制代码
<script setup>
import { ref, reactive, watch } from 'vue';

// 筛选条件
const filters = reactive({
  searchName: '',
  departmentId: null
});

// 分页信息
const pagination = reactive({
  currentPage: 1,
  pageSize: 10
});

// 用户列表数据
const userList = ref([]);
const totalUsers = ref(0);
const isLoading = ref(false);

// 模拟 API 请求
const fetchUsers = async () => {
  isLoading.value = true;
  console.log('正在请求用户列表,参数:', { ...filters, ...pagination });
  // 模拟网络延迟
  await new Promise(resolve => setTimeout(resolve, 800));
  // 模拟返回数据
  userList.value = [`用户1 (页${pagination.currentPage})`, `用户2 (页${pagination.currentPage})`];
  totalUsers.value = 95;
  isLoading.value = false;
};

// 核心逻辑:监听所有会影响列表的参数
watch(
  [
    () => filters.searchName,
    () => filters.departmentId,
    () => pagination.currentPage,
    () => pagination.pageSize
  ],
  // 我们不关心新旧值,只关心变化
  () => {
    fetchUsers();
  },
  { immediate: true } // 初始化时立即加载
);

// 额外的逻辑:当筛选条件变化时,重置页码
watch(
  [() => filters.searchName, () => filters.departmentId],
  () => {
    console.log('筛选条件变化,重置页码到 1');
    pagination.currentPage = 1; // 这会触发上面那个 watch,从而重新请求数据
  }
);
</script>

<template>
  <div>
    <div class="filters">
      <input v-model="filters.searchName" placeholder="按姓名搜索" />
      <select v-model="filters.departmentId">
        <option :value="null">所有部门</option>
        <option :value="1">技术部</option>
        <option :value="2">市场部</option>
      </select>
    </div>

    <div v-if="isLoading">加载中...</div>
    <table v-else>
      <!-- 表格内容 -->
      <tr v-for="user in userList" :key="user">{{ user }}</tr>
    </table>

    <div class="pagination">
      <button @click="pagination.currentPage--" :disabled="pagination.currentPage === 1">上一页</button>
      <span>第 {{ pagination.currentPage }} 页</span>
      <button @click="pagination.currentPage++" :disabled="pagination.currentPage * pagination.pageSize >= totalUsers">下一页</button>
    </div>
  </div>
</template>

模式解读:

这里我们巧妙地使用了两个 watch

  1. watch :监听所有与数据请求相关的参数(筛选、分页),任何一个变化都触发 fetchUsers
  2. 辅助 watch :只监听筛选条件,当它们变化时,它的唯一职责就是重置 currentPage。而 currentPage 的重置,又会触发主 watch,从而实现"筛选条件变化时,自动回到第一页"的需求。

这种分层监听的模式,让逻辑非常清晰,避免了在主 watch 的回调里写一堆复杂的 if-else 判断到底是哪个参数变了。


六、 常见陷阱与最佳实践

即使是经验丰富的开发者,在使用 watch 时也可能踩到一些坑。下面我总结了一些最常见的陷阱,并给出相应的最佳实践建议。

6.1 陷阱一:无限循环

问题描述 :在 watch 的回调函数中,修改了你正在监听的数据源,导致回调被再次触发,从而形成无限循环。

javascript 复制代码
const count = ref(0);

watch(count, (newValue) => {
  console.log(newValue);
  count.value++; // 错误!在回调中修改了被监听的数据
});

这个例子会一直打印数字,直到栈溢出。

如何避免/解决:

  1. 首要原则 :尽量避免在 watch 回调中直接修改被监听的数据源。这通常是逻辑设计上出了问题。
  2. 使用条件判断:如果确实需要在回调中修改,务必加上严格的条件判断,确保只在特定情况下才修改,并且这个修改不会再次触发条件。
  3. 使用 flush: 'sync' 要小心flush: 'sync' 会增加无限循环的风险,因为它会立即执行回调,打断了 Vue 的批量更新机制。除非你非常清楚其影响,否则不要使用。
  4. 考虑使用 computed :如果你只是想根据一个值计算出另一个值,computed 通常是更好的选择,它天生就是为了避免这种副作用而设计的。
6.2 陷阱二:响应式丢失

问题描述 :当你试图解构 reactive 对象或 props 时,可能会丢失响应式连接。

javascript 复制代码
import { reactive, watch } from 'vue';

const state = reactive({ count: 0 });

// 错误示范:直接解构会丢失响应式
const { count } = state;
watch(count, () => { // 这里的 count 是一个普通的数字 0,不是 ref
  console.log('count 变化了'); // 永远不会触发
});

state.count++; // 不会触发 watch

如何避免/解决:

  1. 对于 reactive 对象,使用 getter 函数

    javascript 复制代码
    watch(() => state.count, () => {
      console.log('count 变化了'); // 正确触发
    });
  2. 对于 props,使用 toRefs

    javascript 复制代码
    import { toRefs } from 'vue';
    
    const props = defineProps({ count: Number });
    const { count } = toRefs(props); // toRefs 会将每个属性都转换为 ref
    
    watch(count, () => {
      console.log('props.count 变化了'); // 正确触发
    });

    toRefs 是一个非常有用的工具,它能将一个响应式对象转换为普通对象,其中每个属性都是指向原始对象属性的 ref。这既保留了解构的便利性,又维持了响应式。

6.3 陷阱三:性能滥用

问题描述 :不加选择地使用 deep: true,或者在一个 watch 中执行了非常耗时的操作,导致页面卡顿。

最佳实践:

  1. 优先使用 getter 函数数组:如前所述,这是最高效的方式。只监听你真正关心的数据。
  2. 谨慎使用 deep: true:在必须使用时,思考一下被监听的对象结构是否可以优化,避免过深的嵌套。
  3. 对耗时操作进行防抖/节流 :如果 watch 回调中包含复杂的计算或网络请求,务必使用防抖或节流技术来限制其执行频率。我们前面讲的搜索例子就是防抖的应用。
  4. onUnmounted 中清理 :如果 watch 创建了定时器、事件监听器等副作用,一定要在 onUnmountedonCleanup 中清理它们,防止组件卸载后它们仍在后台运行,消耗资源。

七、 总结:成为 watch 大师

到这里,我们关于 Vue 3 watch 监听多个数据源的深度之旅就接近尾声了。我们一起从"为什么需要"出发,学习了"怎么用"(数组语法、getter 函数),探索了"高级玩法"(配置项、清理、手动停止),并见识了它在各种实战场景中的威力,最后还一起排了排常见的"雷区"。

回顾一下,我们掌握了哪些核心知识点:

  • 核心语法watch([source1, source2], callback),以及回调中 newValuesoldValues 数组的含义。
  • 监听 reactive 对象属性:首选 getter 函数数组,它精确且高效。
  • 灵活混搭refreactive getter、computed 可以自由组合在监听数组中。
  • 配置项deep 用于深度监听,immediate 用于立即执行,flush 用于控制回调在渲染周期中的精确时机。
  • 生命周期管理 :使用 onCleanup 来清理副作用,使用返回的 stop 函数来手动停止监听。
  • 实战模式:表单联动、防抖搜索、分页筛选等。
  • 避坑指南:警惕无限循环、响应式丢失和性能问题。

watch 不仅仅是一个 API,它体现了 Vue 响应式系统的设计哲学:提供强大能力的同时,给予开发者足够的控制权和灵活性。掌握它,意味着你能够更从容地应对复杂的状态管理和副作用处理,让你的 Vue 应用更加健壮、高效和易于维护。

希望这篇文章能成为你学习和使用 watch 的一份可靠参考。前端技术日新月异,但底层的思想和原理是相通的。保持好奇心,多动手实践,你一定能成为驾驭 Vue 响应式系统的大师。祝你编码愉快!

相关推荐
wx_lidysun3 小时前
Nextjs学习笔记
前端·react·next
无羡仙6 小时前
从零构建 Vue 弹窗组件
前端·vue.js
源心锁7 小时前
👋 手搓 gzip 实现的文件分块压缩上传
前端·javascript
源心锁7 小时前
丧心病狂!在浏览器全天候记录用户行为排障
前端·架构
GIS之路7 小时前
GDAL 实现投影转换
前端
phltxy8 小时前
从零入门JavaScript:基础语法全解析
开发语言·javascript
烛阴8 小时前
从“无”到“有”:手动实现一个 3D 渲染循环全过程
前端·webgl·three.js
BD_Marathon8 小时前
SpringBoot——辅助功能之切换web服务器
服务器·前端·spring boot
Kagol8 小时前
JavaScript 中的 sort 排序问题
前端·javascript
eason_fan8 小时前
Service Worker 缓存请求:前端性能优化的进阶利器
前端·性能优化