Vue 3 中 watch 监听多个数据源
javascript import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(count 从 ${oldVal} 变为 ${newVal})
})
数据源可以是:
- 一个 ref 对象
- 一个 reactive 对象的属性(通过 getter 函数访问)
- 一个返回值的 getter 函数
## 监听多个数据源:数组形式
当需要同时监听多个源时,只需将多个源放入一个数组作为 `watch` 的第一个参数。回调函数会收到两个数组:**新值数组**和**旧值数组**,它们按源在数组中的顺序一一对应。
```javascript
import { ref, reactive, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const age = ref(25)
// 同时监听三个 ref
watch(
[firstName, lastName, age],
([newFirst, newLast, newAge], [oldFirst, oldLast, oldAge]) => {
console.log('姓名或年龄发生了变化:')
console.log(`名:${oldFirst} -> ${newFirst}`)
console.log(`姓:${oldLast} -> ${newLast}`)
console.log(`年龄:${oldAge} -> ${newAge}`)
}
)
只要数组中任意一个源的值发生变化,回调就会执行。即使多个源同时改变(例如在同一个事件处理函数中修改它们),回调也只会触发一次,这是 Vue 的异步批量更新机制决定的。
监听多个 reactive 对象的属性
对于 reactive 对象,我们不能直接传递整个对象,否则只会监听该对象引用的变化(一般很少这样做)。正确的做法是使用 getter 函数 返回你想监听的属性。
import { reactive, watch } from 'vue'
const user = reactive({
name: '李四',
settings: {
theme: 'light',
notifications: true
}
})
// 用 getter 提取多个属性
watch(
[
() => user.name,
() => user.settings.theme,
() => user.settings.notifications
],
([newName, newTheme, newNotify], [oldName, oldTheme, oldNotify]) => {
console.log('用户信息变更:')
console.log(`姓名:${oldName} -> ${newName}`)
console.log(`主题:${oldTheme} -> ${newTheme}`)
console.log(`通知:${oldNotify} -> ${newNotify}`)
}
)
深层监听 reactive 对象
如果你确实需要监听整个 reactive 对象的深层变化,而不是仅某个属性,可以将该对象直接放入数组(但通常不推荐,因为会对整个对象做深度监听,性能开销较大):
const state = reactive({ a: 1, b: { c: 2 } })
watch(
[() => state.a, state], // 第二个源是整个 state 的深度监听
([newA, newState], [oldA, oldState]) => {
// 注意:newState 和 oldState 是同一个引用,因为 reactive 对象不会改变自身
// 需要配合 deep: true 才能检测嵌套变化 (默认对 reactive 对象是深层监听的)
}
)
更常见的做法是只监听你关心的属性,保持性能最优。
在数组中使用混合数据源
数组中的每个元素都可以是不同的源类型:ref、reactive 属性 getter、computed 值,甚至普通 getter 函数,它们可以自由组合。
import { ref, reactive, computed, watch } from 'vue'
const keyword = ref('')
const filters = reactive({
category: 'all',
priceRange: [0, 1000]
})
const activeCategory = computed(() => filters.category)
watch(
[
keyword,
() => filters.priceRange,
activeCategory
],
([newKeyword, newRange, newCat], [oldKeyword, oldRange, oldCat]) => {
// 调用搜索 API 或执行其他逻辑
console.log('搜索条件变化,发起请求...')
}
)
配置选项:immediate 和 deep
多源监听同样支持 watch 的第三个参数——选项对象。
立即执行 (immediate)
有时我们需要在组件挂载时就执行一次回调,比如根据多个初始值获取数据。设置 immediate: true 即可:
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
// 首次执行时 oldFirst 和 oldLast 均为 undefined
fetchFullName(newFirst, newLast)
},
{ immediate: true }
)
深层监听 (deep)
当你监听的 getter 返回一个对象或数组,且希望检测其内部变化时,需要开启 deep: true。注意:对 reactive 对象默认就是深层监听,但对 getter 返回的对象则不是。
const list = ref([{ id: 1, text: '吃饭' }])
watch(
[() => list.value],
([newList], [oldList]) => {
// 没有 deep: true 时,仅当 list.value 整个替换才会触发
console.log('列表变化')
},
{ deep: true } // 现在可以监听到数组内部元素的增删改
)
小提示:深层监听会递归遍历所有嵌套属性,产生性能成本,仅在必要时使用。
与 watchEffect 的对比
watchEffect 会自动追踪其同步执行期间访问的所有响应式依赖,因此天然支持“监听多个源”——只要你在回调中访问了它们。
import { watchEffect } from 'vue'
watchEffect(() => {
// 同时依赖 firstName 和 lastName
console.log(`全名:${firstName.value} ${lastName.value}`)
// 任意一个变化都会重新执行
})
何时选择 watch 多源?
| 功能需求 | watch 多源 | watchEffect |
|---|---|---|
| 需要新旧值对比 | ✅ 提供新旧值数组 | ❌ 无法获取旧值 |
| 指定具体监听哪些依赖 | ✅ 显式声明源数组 | ❌ 自动收集,可能包含多余依赖 |
| 懒执行(不立即运行) | ✅ 默认不立即,可配置 | ❌ 立即执行 |
| 异步操作前对比值 | ✅ 方便 | ❌ 需额外保存旧值 |
因此,当你需要精细控制依赖或对比新旧值时,使用 watch 监听多个源更合适。
实战示例:表单联动校验
假设一个注册表单,需要在用户名、邮箱或密码任意一项变化时,重新计算表单有效性并显示错误信息。
<template>
<form @submit.prevent="submit">
<input v-model="username" placeholder="用户名" />
<input v-model="email" placeholder="邮箱" />
<input v-model="password" type="password" placeholder="密码" />
<span v-if="errors.length">{{ errors.join(', ') }}</span>
<button :disabled="!isValid">注册</button>
</form>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
const username = ref('')
const email = ref('')
const password = ref('')
const errors = ref([])
// 计算属性也可以作为源
const isEmailValid = computed(() => /\S+@\S+\.\S+/.test(email.value))
watch(
[username, isEmailValid, password],
([newName, emailOk, newPass], [oldName, oldEmailOk, oldPass]) => {
const newErrors = []
if (newName.length < 3) newErrors.push('用户名至少3个字符')
if (!emailOk) newErrors.push('邮箱格式不正确')
if (newPass.length < 6) newErrors.push('密码至少6位')
errors.value = newErrors
}
)
const isValid = computed(() => errors.value.length === 0)
function submit() {
if (isValid.value) {
// 发送注册请求
}
}
</script>