Code前端首页关于Code前端联系我们

Vue3 里怎么用 watch 同时监听多个数据源?

terry 3小时前 阅读数 8 #Vue
文章标签 Vue3;watch多监听

在写Vue3项目的时候,很多同学会碰到需要同时监听多个数据变化的场景,比如表单里多个字段联动、页面状态和路由参数一起变化时执行逻辑……那Vue3的watch到底怎么实现多数据源监听?不同情况要注意什么?今天就把这块儿掰开揉碎讲清楚。

watch监听多个数据源的基础写法

Vue3的watch API支持把多个监听源放到一个数组里,不管这些源是refreactive的属性,还是函数返回的响应式值,只要有一个变化,回调就会触发,先看最基础的例子:

import { ref, watch } from 'vue'
// 定义两个ref变量
const searchKey = ref('')
const page = ref(1)
// 把要监听的源放进数组
watch([searchKey, page], (newValues, oldValues) => {
  console.log('搜索关键词或页码变了:', newValues, oldValues)
  // newValues是 [searchKey新值, page新值]
  // oldValues是 [searchKey旧值, page旧值]
})
// 模拟数据变化
searchKey.value = 'Vue3' // 触发回调
page.value = 2 // 触发回调

这里有几个关键点:

  • 监听源是数组形式,数组里可以放ref、返回响应式值的函数(() => reactiveObj.prop )、computed等;
  • 回调函数的第一个参数newValues新值数组,顺序和监听源数组一一对应;第二个参数oldValues旧值数组,顺序也完全一致;
  • 只要数组里任意一个源发生变化,回调就会执行。

不同数据源类型的监听细节

Vue3里响应式数据分refreactivecomputed等类型,不同类型在多数据源监听时,写法和注意事项不一样,得“对症下药”。

监听ref类型的多个数据源

ref是最常用的基础响应式类型(比如基本类型string/number,或对象/数组),监听多个ref时,直接把ref变量放进数组即可:

const count = ref(0)
const flag = ref(false)
watch([count, flag], (newVals, oldVals) => {
  // count或flag变化时触发
})

但要注意:如果ref包裹的是对象/数组(比如const list = ref([])),修改对象内部属性(list.value.push(1))不会改变list的引用,这时候watch默认不会触发(因为refvalue引用没变化),这时候需要配合{ deep: true }开启深层监听:

const list = ref([{ id: 1, name: 'A' }])
watch([list], (newVal, oldVal) => {
  console.log('list内部变化了')
}, { deep: true }) // 开启深层监听
list.value[0].name = 'B' // 触发回调

监听reactive对象的多个属性

reactive用于创建深层响应式对象(比如复杂的用户信息、表单数据),但监听reactive对象时,不能直接把对象属性丢进数组,因为reactive的属性不是ref,Vue无法跟踪“属性访问”的依赖,这时候得用函数返回值的形式:

const user = reactive({
  name: '张三',
  settings: {
    darkMode: false,
    lang: 'zh'
  }
})
// 错误写法:直接传user.name,无法触发监听
watch([user.name, user.settings.darkMode], () => {}) 
// 正确写法:用函数返回要监听的属性
watch([() => user.name, () => user.settings.darkMode], (newVals, oldVals) => {
  console.log('用户名或深色模式变了', newVals, oldVals)
})

如果想监听整个reactive对象的所有变化(比如user的任意属性修改),可以直接传reactive对象,并开启deep: true

watch(user, (newUser, oldUser) => {
  console.log('user任意属性变化', newUser, oldUser)
}, { deep: true }) 
user.settings.lang = 'en' // 触发回调

但这种写法性能开销大(因为深层遍历整个对象),所以更推荐“精确监听需要的属性”,减少不必要的响应式跟踪。

结合computed的情况

computed本身是“响应式的计算结果”,可以直接当作监听源,比如有个计算属性整合了多个数据,同时监听计算属性和其他源:

const first = ref('')
const last = ref('')
const fullName = computed(() => `${first.value} ${last.value}`)
watch([fullName, someOtherRef], (newVals, oldVals) => {
  // fullName变化(即first或last变化),或someOtherRef变化时触发
})

computed作为监听源时,逻辑和ref类似——它的变化由依赖的响应式数据驱动,watch能自动感知。

多数据源监听时的回调逻辑设计

当多个数据源同时变化时,怎么在回调里区分“到底是哪个数据变了”?怎么处理新旧值?这里有几个实用技巧:

区分变化的数据源

回调里的newValuesoldValues数组,顺序和监听源数组一一对应,比如监听[A, B, C]newValues[0]A的新值,newValues[1]B的新值……所以可以通过数组索引判断哪个源变化:

watch([name, age, city], (newVals, oldVals) => {
  if (newVals[0] !== oldVals[0]) {
    console.log('name变了')
  }
  if (newVals[1] !== oldVals[1]) {
    console.log('age变了')
  }
  // ...
})

但要注意:如果多个源同时变化(比如一次操作里nameage都改了),回调只会执行一次(因为Vue的响应式更新是“批处理”的,多个依赖变化会合并触发)。

处理对象/数组的新旧值

如果监听源是对象或数组(比如ref包裹的对象、reactive的属性),oldValuesnewValues可能是同一个引用(因为修改对象内部属性不会改变引用),这时候要拿到“真正的旧值”,需要手动处理:

  • 开启deep: true + 深拷贝旧值
    const info = ref({ city: '北京' })
    watch([info], (newVal, oldVal) => {
    // 深拷贝旧值(比如用JSON.parse(JSON.stringify(oldVal)))
    const oldInfo = JSON.parse(JSON.stringify(oldVal[0]))
    console.log('旧城市:', oldInfo.city, '新城市:', newVal[0].city)
    }, { deep: true })

info.value.city = '上海' // 触发回调,能拿到正确旧值


- 方法二:拆分监听具体属性(推荐)  
直接监听基本类型的属性(() => info.value.city`),这样新旧值是基本类型,不会有引用问题:  
```js
watch([() => info.value.city], (newCity, oldCity) => {
  console.log('城市从', oldCity[0], '变到', newCity[0])
})

实际项目中的应用场景

光讲语法不够,结合真实场景才能理解“为什么需要多数据源监听”,分享3个常见场景,看看watch怎么解决问题。

表单多字段联动验证

注册/登录表单里,往往需要同时验证“用户名、密码、确认密码”等多个字段,实时更新按钮状态,用watch监听多个输入框的变化:

<template>
  <div class="form">
    <input v-model="username" placeholder="请输入用户名" />
    <input v-model="password" type="password" placeholder="请输入密码" />
    <input v-model="confirmPwd" type="password" placeholder="请确认密码" />
    <button :disabled="!canSubmit">提交</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
const username = ref('')
const password = ref('')
const confirmPwd = ref('')
const canSubmit = ref(false)
// 同时监听三个输入框
watch([username, password, confirmPwd], () => {
  // 验证逻辑:用户名非空 + 密码≥6位 + 两次密码一致
  const nameValid = username.value.trim().length > 0
  const pwdValid = password.value.length >= 6
  const pwdSame = password.value === confirmPwd.value
  canSubmit.value = nameValid && pwdValid && pwdSame
})
</script>

这样用户每输入一个字符,都会实时验证,按钮状态自动更新,体验更流畅。

页面状态与路由参数联动

列表页中,“搜索关键词”(前端状态)和“当前页码”(路由参数)变化时,都需要重新请求接口,用watch同时监听ref和路由参数:

import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const searchKey = ref('')
watch([searchKey, () => route.query.page], (newVals, oldVals) => {
  const newKey = newVals[0]
  const newPage = newVals[1]
  // 调用接口请求数据
  fetchList(newKey, newPage)
})
// 模拟搜索关键词变化
searchKey.value = 'Vue' // 触发请求
// 模拟路由参数变化(比如用户点击分页按钮,路由page从1→2)
// 路由变化时,route.query.page自动更新,触发watch

这里route.query.page是路由的响应式数据,用函数() => route.query.page包裹,确保watch能跟踪其变化。

多状态控制复杂UI

后台管理系统中,“深色模式”(darkMode)和“侧边栏展开状态”(isSidebarOpen)变化时,需要同时更新页面样式和布局,用watch监听两个状态:

import { reactive, watch } from 'vue'
const appState = reactive({
  darkMode: false,
  isSidebarOpen: true
})
watch([() => appState.darkMode, () => appState.isSidebarOpen], () => {
  // 更新页面样式(比如给body加dark类)
  document.body.classList.toggle('dark', appState.darkMode)
  // 更新侧边栏样式(比如宽度、动画)
  updateSidebarStyle(appState.isSidebarOpen)
})
// 模拟主题切换
appState.darkMode = true // 触发样式更新
// 模拟侧边栏收起
appState.isSidebarOpen = false // 触发样式更新

这种场景下,多个状态共同决定UI,watch能统一处理变化后的逻辑,避免代码分散。

常见问题与避坑指南

实际开发中,很多同学会碰到“监听不触发”“新旧值一样”“重复执行”等问题,这里总结5个高频坑,教你快速解决。

监听reactive对象时,回调不触发?

原因:监听reactive对象的单个属性时,没用到“函数返回值”的形式,导致Vue无法跟踪依赖。

错误示例:

const user = reactive({ name: '张三' })
watch([user.name], () => {}) // 错误!user.name不是ref,watch无法跟踪

正确写法:

watch([() => user.name], () => {}) // 用函数返回要监听的属性

多个数据源同时变化,回调执行多次?

Vue的响应式系统是批处理更新的——同一轮事件循环中,多个依赖变化会合并触发,所以回调只会执行一次。

watch([a, b], () => {
  console.log('触发')
})
a.value = 1
b.value = 2
// 回调只执行一次,因为a和b的变化在同一轮更新中

如果回调执行多次,大概率是因为代码里有异步操作(比如setTimeoutPromise.then)导致更新分散到多轮,这时候可以用watchflush选项控制执行时机(比如flush: 'sync'强制同步执行,但谨慎使用,会影响性能)。

oldVal和newVal完全一样?

原因:监听的是对象/数组的引用,修改内部属性不会改变引用,所以新旧值是同一个对象。

解决方法:

  • 拆分监听具体属性(比如监听() => obj.propprop是基本类型);
  • 开启deep: true深拷贝旧值(适合监听整个对象的场景);
  • ref包裹对象时,修改整个refvalue(比如obj.value = { ...obj.value, prop: 'new' }),这样引用变化,watch能拿到新值。

怎么停止watch的监听?

watch调用后会返回一个停止函数,调用它就能停止监听,通常在组件卸载时执行:

import { onUnmounted, watch } from 'vue'
const stopWatch = watch([a, b], () => {})
onUnmounted(() => {
  stopWatch() // 组件卸载时停止监听,避免内存泄漏
})

监听computed时不触发?

computed的变化由依赖的响应式数据驱动,如果computed没触发,先检查:

  • 依赖的响应式数据是否真的变化了?
  • computed是否有缓存(比如依赖没变化时,computed不会重新计算)?

举个例子:

const num = ref(1)
const double = computed(() => num.value * 2)
watch(double, () => {
  console.log('double变了')
})
num.value = 2 // 触发double更新,watch回调执行
num.value = 2 // 依赖没变化,computed不更新,watch不触发

和watchEffect的区别(延伸知识点)

很多同学分不清watchwatchEffect,这里简单对比“多数据源监听”场景下的用法:

特性 watch watchEffect
监听源 必须显式声明要监听的源 自动收集依赖(回调里用到的响应式数据)
新旧值 能拿到新旧值 拿不到旧值,只有当前值
执行时机 数据源变化时执行 初始化时执行一次,之后数据源变化时执行
精确性 只监听声明的源,更可控 依赖自动收集,可能包含多余数据源

举个例子,用watchEffect实现“监听nameage”:

const name = ref('')
const age = ref(0)
watchEffect(() => {
  console.log('name或age变了:', name.value, age.value)
})

这种写法更简洁,但无法区分到底是name还是age变化,也拿不到旧值,所以场景不同,选择不同:

  • 需要精确控制监听源、需要新旧值 → 用watch
  • 只需要“响应式数据变化时执行逻辑”,不需要区分源 → 用watchEffect

看完这些,再碰到“同时监听多个数据”的需求,是不是心里有底了?记住核心逻辑:用数组装监听源,区分`ref`/`reactive`/`computed`的写法差异,处理好新旧值和回调逻辑,实际项目里多试试不同场景,踩过坑才记得牢~如果还有其他疑问,评论区随时聊~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门