Vue3 里怎么用 watch 同时监听多个数据源?
在写Vue3项目的时候,很多同学会碰到需要同时监听多个数据变化的场景,比如表单里多个字段联动、页面状态和路由参数一起变化时执行逻辑……那Vue3的watch到底怎么实现多数据源监听?不同情况要注意什么?今天就把这块儿掰开揉碎讲清楚。
watch监听多个数据源的基础写法
Vue3的watch
API支持把多个监听源放到一个数组里,不管这些源是ref
、reactive
的属性,还是函数返回的响应式值,只要有一个变化,回调就会触发,先看最基础的例子:
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里响应式数据分ref
、reactive
、computed
等类型,不同类型在多数据源监听时,写法和注意事项不一样,得“对症下药”。
监听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
默认不会触发(因为ref
的value
引用没变化),这时候需要配合{ 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
能自动感知。
多数据源监听时的回调逻辑设计
当多个数据源同时变化时,怎么在回调里区分“到底是哪个数据变了”?怎么处理新旧值?这里有几个实用技巧:
区分变化的数据源
回调里的newValues
和oldValues
是数组,顺序和监听源数组一一对应,比如监听[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变了') } // ... })
但要注意:如果多个源同时变化(比如一次操作里name
和age
都改了),回调只会执行一次(因为Vue的响应式更新是“批处理”的,多个依赖变化会合并触发)。
处理对象/数组的新旧值
如果监听源是对象或数组(比如ref
包裹的对象、reactive
的属性),oldValues
和newValues
可能是同一个引用(因为修改对象内部属性不会改变引用),这时候要拿到“真正的旧值”,需要手动处理:
- 开启
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的变化在同一轮更新中
如果回调执行多次,大概率是因为代码里有异步操作(比如setTimeout
、Promise.then
)导致更新分散到多轮,这时候可以用watch
的flush
选项控制执行时机(比如flush: 'sync'
强制同步执行,但谨慎使用,会影响性能)。
oldVal和newVal完全一样?
原因:监听的是对象/数组的引用,修改内部属性不会改变引用,所以新旧值是同一个对象。
解决方法:
- 拆分监听具体属性(比如监听
() => obj.prop
,prop
是基本类型); - 开启
deep: true
并深拷贝旧值(适合监听整个对象的场景); - 用
ref
包裹对象时,修改整个ref
的value
(比如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的区别(延伸知识点)
很多同学分不清watch
和watchEffect
,这里简单对比“多数据源监听”场景下的用法:
特性 | watch | watchEffect |
---|---|---|
监听源 | 必须显式声明要监听的源 | 自动收集依赖(回调里用到的响应式数据) |
新旧值 | 能拿到新旧值 | 拿不到旧值,只有当前值 |
执行时机 | 数据源变化时执行 | 初始化时执行一次,之后数据源变化时执行 |
精确性 | 只监听声明的源,更可控 | 依赖自动收集,可能包含多余数据源 |
举个例子,用watchEffect
实现“监听name
和age
”:
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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。