Vue 3 中 watch 监听多个数据源

FreeGuideOnline 最新 2026-07-07

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>