Skip to content

在 Vue 3 的 watch API 中,源(source) 是你指定要侦听的目标。watch 的灵活性很大程度上源于其对多种数据源的支持。理解 watch 可以接受哪些类型的 source,是高效使用侦听器的关键。

watchsource 参数可以是以下五种类型:

  1. Ref 对象
  2. Reactive 对象
  3. Getter 函数
  4. 源数组(Array of Sources)
  5. 字符串路径(仅限 setup 之外,如选项式 API)

一、1. Ref 对象

这是最直接的用法。你可以直接将一个 ref 作为 source

用法

js
import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newVal, oldVal) => {
  console.log(`Count changed: ${oldVal} -> ${newVal}`);
});

count.value++; // 输出: "Count changed: 0 -> 1"

特点

  • 侦听的是 .valuewatch 实际侦听的是 ref 内部的 value
  • 基本类型:对于 numberstringboolean 等,使用 === 比较新旧值。
  • 对象类型:如果 ref 包含对象,watch 默认只侦听 ref 引用的变化。若要侦听对象内部变化,需使用 deep: true
js
const user = ref({ name: 'Alice', age: 25 });

// 默认:只侦听 user 引用变化
watch(user, (newUser, oldUser) => {
  console.log('User reference changed');
});

// 深度侦听:user.age 变化也会触发
watch(user, (newUser, oldUser) => {
  console.log('User details changed');
}, { deep: true });

二、2. Reactive 对象

你可以将一个由 reactive() 创建的响应式对象作为 source

用法

js
import { reactive, watch } from 'vue';

const state = reactive({
  count: 0,
  name: 'Vue'
});

watch(state, (newState, oldState) => {
  console.log('State changed');
  // 注意:newState 和 oldState 都是同一个响应式对象的引用
  // 无法直接比较深层变化
}, { deep: true }); // deep 必须为 true 才能侦听到内部变化

state.count++; // 触发

特点

  • 必须 deep: truereactive 对象本身是响应式的,但 watch 默认不会递归侦听其嵌套属性。必须设置 deep: true 才能侦听到内部变化。
  • 新旧值引用相同:由于 reactive 是代理对象,newStateoldState 指向同一个对象。你无法通过 newState !== oldState 来判断变化。通常需要在回调内部手动比较具体属性,或依赖 deep 机制。
  • 性能考虑:深度侦听大型对象可能影响性能。

三、3. Getter 函数

这是最灵活和强大的用法。你可以传入一个函数,该函数返回你想要侦听的值。

用法

js
import { ref, watch } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// 侦听计算出的 fullName
watch(
  () => `${firstName.value} ${lastName.value}`,
  (newName, oldName) => {
    console.log(`Name changed: ${oldName} -> ${newName}`);
  }
);

firstName.value = 'Jane'; // 输出: "Name changed: John Doe -> Jane Doe"

特点

  • 按需计算getter 函数只在 watch 需要时执行(如初始化或依赖变化后)。
  • 自动依赖收集getter 函数内部访问的任何响应式数据都会被自动追踪为依赖。
  • 适合复杂逻辑:可以组合多个 ref、调用 computed、或执行复杂计算。
  • 惰性求值:值只在需要时计算。

四、4. 源数组(Array of Sources)

你可以传入一个数组,侦听多个源的变化。回调函数会接收一个包含所有新值的数组和一个包含所有旧值的数组。

用法

js
import { ref, watch } from 'vue';

const count = ref(0);
const name = ref('Vue');

watch(
  [count, name], // 侦听多个源
  ([newCount, newName], [oldCount, oldName]) => {
    console.log(`Count: ${newCount}, Name: ${newName}`);
    // 可以比较新旧值
  }
);

count.value++; // 触发
name.value = 'React'; // 触发

特点

  • 任意组合:数组中的元素可以是 refreactive 对象或 getter 函数。
  • 统一回调:任何一个源变化,都会触发同一个回调。
  • 新旧值数组:回调参数是数组,便于批量处理。
  • 常见场景
    • 表单验证(多个输入框)。
    • 联动逻辑(一个状态变化影响多个其他状态)。
js
// 混合类型
watch(
  [
    () => user.value.role, // getter
    permissions,           // reactive
    isActive               // ref
  ],
  ([newRole, newPerms, newActive]) => {
    updateAccessControl(newRole, newPerms, newActive);
  }
);

五、5. 字符串路径(仅限选项式 API)

选项式 APIsetup 之外)中,watch 支持以字符串路径的形式侦听嵌套对象的属性。

用法(选项式 API)

js
export default {
  data() {
    return {
      user: {
        profile: {
          name: 'Alice'
        }
      }
    };
  },
  watch: {
    // 字符串路径
    'user.profile.name'(newVal, oldVal) {
      console.log(`Name changed: ${oldVal} -> ${newVal}`);
    }
  }
};

特点

  • 仅限选项式 API:在 setup<script setup> 中,不能使用字符串路径。
  • 方便性:无需写 getter 函数即可侦听深层属性。
  • 局限性:路径是静态的,不支持动态键。

在组合式 API 中,应使用 getter 函数替代:

js
// 组合式 API 中的等效写法
watch(
  () => user.value.profile.name,
  (newName, oldName) => {
    console.log(`Name changed: ${oldName} -> ${newName}`);
  }
);

六、总结:source 类型速查表

Source 类型适用场景是否需要 deep新旧值可用示例
Ref基本类型或对象引用对象内部变化需 deepwatch(count, callback)
Reactive响应式对象✅ 必须 deep 才能侦听内部✅ (但引用相同)watch(state, callback, { deep: true })
Getter 函数复杂计算、组合多个源根据函数返回值决定watch(() => a.value + b.value, callback)
源数组同时侦听多个独立源数组元素可能需要✅ (数组形式)watch([count, name], callback)
字符串路径选项式 API 中侦听深层属性✅ 通常需要'user.profile.name': callback

七、最佳实践

  1. 优先使用 getter 函数:在组合式 API 中,getter 函数是最灵活、最推荐的方式。
  2. reactive 对象务必 deep: true:否则无法侦听到内部变化。
  3. 避免过度侦听:只侦听必要的数据,减少性能开销。
  4. 利用源数组:当多个源触发相同逻辑时,使用数组避免重复代码。

掌握 watchsource 类型,你就能精确地控制侦听的粒度,编写出既高效又可维护的响应式逻辑。