在 Vue 3 的 watch API 中,源(source) 是你指定要侦听的目标。watch 的灵活性很大程度上源于其对多种数据源的支持。理解 watch 可以接受哪些类型的 source,是高效使用侦听器的关键。
watch 的 source 参数可以是以下五种类型:
Ref对象Reactive对象- Getter 函数
- 源数组(Array of Sources)
- 字符串路径(仅限 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"特点
- 侦听的是
.value:watch实际侦听的是ref内部的value。 - 基本类型:对于
number、string、boolean等,使用===比较新旧值。 - 对象类型:如果
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: true:reactive对象本身是响应式的,但watch默认不会递归侦听其嵌套属性。必须设置deep: true才能侦听到内部变化。 - 新旧值引用相同:由于
reactive是代理对象,newState和oldState指向同一个对象。你无法通过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'; // 触发特点
- 任意组合:数组中的元素可以是
ref、reactive对象或getter函数。 - 统一回调:任何一个源变化,都会触发同一个回调。
- 新旧值数组:回调参数是数组,便于批量处理。
- 常见场景:
- 表单验证(多个输入框)。
- 联动逻辑(一个状态变化影响多个其他状态)。
js
// 混合类型
watch(
[
() => user.value.role, // getter
permissions, // reactive
isActive // ref
],
([newRole, newPerms, newActive]) => {
updateAccessControl(newRole, newPerms, newActive);
}
);五、5. 字符串路径(仅限选项式 API)
在选项式 API(setup 之外)中,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 | 基本类型或对象引用 | 对象内部变化需 deep | ✅ | watch(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 |
七、最佳实践
- 优先使用
getter函数:在组合式 API 中,getter函数是最灵活、最推荐的方式。 reactive对象务必deep: true:否则无法侦听到内部变化。- 避免过度侦听:只侦听必要的数据,减少性能开销。
- 利用源数组:当多个源触发相同逻辑时,使用数组避免重复代码。
掌握 watch 的 source 类型,你就能精确地控制侦听的粒度,编写出既高效又可维护的响应式逻辑。