一、Vue3 watch 监听多个源的基础语法
p>在Vue3项目开发里,经常会遇到需要同时关注多个数据变化的场景,比如表单里多个输入框联动验证、购物车中商品数量和优惠活动的联动计算……这时候就需要用watch监听多个数据源,可Vue3的watch怎么实现多数据源监听?不同类型的数据源(像ref、reactive、计算属性)组合监听时要注意啥?深度监听、立即执行这些配置在多源场景下咋用?今天就把这些问题掰开了揉碎了讲清楚。
先回忆单个源的watch用法:watch(源, 回调)
能追踪单个数据变化,但业务里常需同时盯多个数据,这时候要把多个数据源放进数组,回调函数的参数也会变成“新值数组”和“旧值数组”——顺序和监听数组里的数据源一一对应。
监听多个 ref 的例子
比如页面有“姓名”和“年龄”两个输入框,要实时打印值的变化:
<template> <div> <input v-model="name" placeholder="姓名" /> <input v-model="age" type="number" placeholder="年龄" /> </div> </template> <script setup> import { ref, watch } from 'vue' const name = ref('') const age = ref(0) watch([name, age], ([newName, newAge], [oldName, oldAge]) => { console.log('姓名从', oldName, '变成', newName) console.log('年龄从', oldAge, '变成', newAge) }) </script>
这里要注意:多个源的新值、旧值都是数组,顺序和 watch
第一个参数的数组顺序严格对应。
监听 reactive 对象的属性(必须用 getter 函数)
如果要监听 reactive
对象的某个属性,直接把属性丢进数组会失效——因为 reactive
是“代理对象”,watch 无法直接追踪普通属性的读取行为,这时候得用返回属性的函数(getter) 来让 watch 感知变化:
<script setup> import { reactive, watch } from 'vue' const user = reactive({ name: '', age: 0 }) watch([() => user.name, () => user.age], ([newName, newAge], [oldName, oldAge]) => { // 当 user.name 或 user.age 变化时触发 }) </script>
背后原理是 Vue3 的响应式依赖追踪逻辑:只有把“读取属性”的操作包在函数里,watch 执行时才会调用这个函数,进而建立依赖关系。
不同类型数据源组合监听的细节
Vue3 里数据源分 ref
、reactive
、computed
(计算属性)等,组合监听时要注意各自特性:
ref + ref 的监听
两个 ref
变量直接放数组里即可,因为 ref
的 .value
会被自动解包(setup 语法糖下无需手动写 .value
),watch 能自动追踪其变化,像前面“姓名+年龄”的例子,就是典型的 ref+ref
监听。
reactive + reactive 的监听
如果直接监听整个 reactive
对象,
const user1 = reactive({ name: 'A', age: 18 }) const user2 = reactive({ name: 'B', age: 20 }) watch([user1, user2], ([newU1, newU2], [oldU1, oldU2]) => { // 这里 newU1 和 oldU1 是同一个对象(因为 reactive 是引用类型) // 所以只有当 user1 被重新赋值(如 user1 = reactive({...}))时才会触发,属性变化不触发 })
这意味着:监听 reactive
对象的“属性变化”,必须用 getter 函数返回具体属性;如果想监听整个对象的“引用替换”,才直接放 reactive
对象。
ref + reactive 的混合监听
比如同时监听一个 ref
变量和一个 reactive
对象的属性:
const keyword = ref('') // ref 变量 const user = reactive({ name: '' }) // reactive 对象 watch([keyword, () => user.name], ([newKey, newName], [oldKey, oldName]) => { // keyword 是 ref,直接放数组;user.name 是 reactive 属性,用 getter 函数 })
这种混合场景里,ref
直接放数组,reactive
属性用 getter,就能让两个不同类型的源都被正确追踪。
computed 作为数据源
计算属性(computed
)本质是带 .value
的响应式对象(和 ref
类似),所以监听多个 computed
时,逻辑和 ref
一致——直接放数组里即可:
const name = ref('') const age = ref(0) const info = computed(() => `${name.value} - ${age.value}`) // 计算属性 const status = computed(() => age.value > 18 ? '成年' : '未成年') // 计算属性 watch([info, status], ([newInfo, newStatus], [oldInfo, oldStatus]) => { // 当 name、age 变化导致 info 或 status 变化时,这里会触发 })
多源监听时的深度监听(deep)
深度监听用来处理对象/数组的“深层属性变化”,但多个源场景下配置 deep
要注意场景差异:
什么时候需要 deep?
-
监听
ref
包裹的对象/数组,且想追踪其内部属性变化时,需要deep: true
。const settings = ref({ display: { theme: 'light', font: 14 }, notifications: { enable: true } }) watch(settings, (newVal, oldVal) => { // settings.display.theme 变化时触发(因为 deep: true) }, { deep: true })
-
监听
reactive
对象时,不需要deep
,因为reactive
本身是“深层响应式”的——任何嵌套层级的属性变化都会被追踪。const user = reactive({ name: '张三', info: { address: '北京' } }) watch(user, (newU, oldU) => { // 当 user.info.address 变化时,这里会自动触发(无需 deep) })
多源场景下 deep 的配置逻辑
deep
是对整个 watch 配置生效的,但只有“对象/数组类型的源”会受影响,比如同时监听 ref
对象和普通 ref
数字:
const settings = ref({ /* 深层结构 */ }) // ref 包裹的对象 const count = ref(0) // 普通 ref 数字 watch([settings, count], ([newSet, newCount], [oldSet, oldCount]) => { // settings 的深层变化需要 deep,count 不需要 }, { deep: true })
这里 deep: true
只影响 settings
(因为 count
是基本类型,deep 对它无效),但这种“一刀切”的配置可能不够灵活,建议用 getter 函数精确监听属性,减少对 deep 的依赖(毕竟 deep 会遍历对象所有属性,可能影响性能)。
多源监听的立即执行(immediate)
immediate: true
能让 watch 在初始化时就执行一次回调,而不是等数据源变化后才执行,多源场景下,适合“页面加载时根据初始数据执行逻辑”(比如初始化请求)。
例子:页面加载时触发搜索请求
const keyword = ref('') const category = ref('all') watch([keyword, category], ([newKey, newCat], [oldKey, oldCat]) => { // 发送请求获取列表数据 console.log('请求参数:', newKey, newCat) }, { immediate: true })
初始化时,keyword
和 category
的初始值会被当作 new
值,old
值是 undefined
(因为第一次执行没有旧值),所以回调里要注意处理旧值可能为 undefined
的情况:
watch([keyword, category], ([newKey, newCat], [oldKey, oldCat]) => { if (oldKey !== undefined) { // 非初始化执行(值变化触发) console.log('值变化,重新请求') } else { // 初始化执行(页面加载时) console.log('页面加载,初始化请求') } }, { immediate: true })
手动停止多源 watch 的监听
Vue3 的 watch 在组件卸载时会自动停止,但如果需要在组件生命周期内手动停止(比如表单提交后不再监听),可以用 watch 返回的“停止函数”:
<script setup> import { ref, watch } from 'vue' const name = ref('') const age = ref(0) // watch 返回停止函数 const stopWatch = watch([name, age], (newVals, oldVals) => { // 业务逻辑 }) // 某个时机手动停止(比如按钮点击) const handleStop = () => { stopWatch() // 调用停止函数,后续变化不再触发回调 } </script>
如果有多个 watch,每个都要单独管理停止函数,手动停止能避免不必要的性能消耗(比如组件隐藏后还在监听数据)。
实际开发场景:多源 watch 的典型用法
光讲语法不够,结合真实场景才能理解价值,下面举 3 个常见案例:
场景1:表单多字段联动验证
注册表单有“用户名、密码、确认密码”三个字段,需实时验证:
- 用户名长度 ≥3
- 密码长度 ≥6
- 确认密码和密码一致
<template> <form> <input v-model="username" placeholder="用户名" /> <input v-model="password" type="password" placeholder="密码" /> <input v-model="confirmPwd" type="password" placeholder="确认密码" /> <div v-if="usernameError">用户名长度需≥3</div> <div v-if="passwordError">密码长度需≥6</div> <div v-if="confirmError">两次密码不一致</div> </form> </template> <script setup> import { ref, watch, computed } from 'vue' const username = ref('') const password = ref('') const confirmPwd = ref('') // 用 computed 封装验证逻辑 const usernameError = computed(() => username.value.length < 3) const passwordError = computed(() => password.value.length < 6) const confirmError = computed(() => password.value !== confirmPwd.value) // 监听多个验证结果,初始化就执行验证 watch([usernameError, passwordError, confirmError], ([newUserErr, newPwdErr, newConfirmErr]) => { console.log('表单验证状态:', newUserErr, newPwdErr, newConfirmErr) // 这里可以统一处理“是否允许提交”等逻辑 }, { immediate: true }) </script>
场景2:购物车商品数量与优惠计算
购物车中,商品数量变化、优惠券选择变化,都要重新计算最终价格:
<template> <div> <div v-for="(item, index) in cartItems" :key="index"> {{ item.name }} - 数量:<input v-model.number="item.quantity" type="number" /> </div> <select v-model="coupon"> <option value="0">无优惠</option> <option value="10">满100减10</option> <option value="20">满200减20</option> </select> <div>最终价格:{{ finalPrice }}</div> </div> </template> <script setup> import { reactive, ref, watch, computed } from 'vue' const cartItems = reactive([ { name: '商品A', price: 50, quantity: 1 }, { name: '商品B', price: 80, quantity: 1 } ]) const coupon = ref('0') // 计算商品总价 const totalPrice = computed(() => { return cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0) }) // 计算最终价格(结合优惠券) const finalPrice = computed(() => { let base = totalPrice.value if (coupon.value === '10' && base >= 100) base -= 10 if (coupon.value === '20' && base >= 200) base -= 20 return base }) // 监听购物车(reactive 数组)和优惠券(ref)的变化 watch([cartItems, coupon], () => { console.log('购物车或优惠券变化,重新计算价格') // 这里无需手动更新 finalPrice,因为 computed 会自动响应依赖变化 }) </script>
场景3:多条件搜索的请求触发
页面有“搜索关键词、分类筛选、价格区间”三个条件,任意一个变化都要重新请求接口:
<template> <div> <input v-model="keyword" placeholder="搜索关键词" /> <select v-model="category"> <option value="all">全部分类</option> <option value="electronics">数码</option> <option value="clothes">服饰</option> </select> <div>价格区间:<input v-model.number="minPrice" type="number" /> - <input v-model.number="maxPrice" type="number" /></div> </div> </template> <script setup> import { ref, watch } from 'vue' const keyword = ref('') const category = ref('all') const minPrice = ref(0) const maxPrice = ref(Infinity) // 发送请求的函数 const fetchData = () => { console.log('发送请求,参数:', { keyword, category, minPrice, maxPrice }) // 实际开发中用 axios 等工具发请求 } // 监听四个数据源,任意一个变化就触发请求 watch([keyword, category, minPrice, maxPrice], () => { fetchData() }, { immediate: false }) // 也可以设为 true,页面加载时就请求 </script>
常见问题与避坑指南
实际开发中,多源 watch 容易踩这些“坑”,提前避开能少走弯路:
监听 reactive 属性时,用错语法导致不触发
错误写法(直接放属性,不包 getter 函数):
const user = reactive({ name: '张三', age: 18 }) watch([user.name, user.age], (newVals) => { // 不会触发!因为 user.name 是普通值,watch 无法追踪 })
正确写法(用 getter 函数包裹):
watch([() => user.name, () => user.age], (newVals) => { // 正确触发 })
多个源的新旧值顺序搞反
watch 回调的参数是 (newValues, oldValues)
,数组顺序和监听源数组严格一致,比如监听 [a, b]
,newValues
是 [newA, newB]
,oldValues
是 [oldA, oldB]
——别把顺序写反了。
deep 配置滥用导致性能问题
如果监听的是 ref
包裹的大对象,设置 deep: true
会遍历对象所有属性,性能开销大,尽量用 getter 函数精确监听需要的属性,减少对 deep 的依赖。
组件卸载后 watch 还在执行
虽然 Vue3 的 watch 会在组件卸载时自动停止,但如果回调里有异步操作(setTimeout、Promise),要手动停止避免内存泄漏:
const stop = watch([a, b], () => { setTimeout(() => { // 组件卸载后可能还会执行 }, 1000) }) onUnmounted(() => { stop() // 手动停止,避免异步回调执行 })
Vue3 的 watch 监听多个数据源,核心是把要监听的源(ref、reactive 的 getter、computed 等)放进数组,回调里处理“新值数组”和“旧值数组”,不同类型数据源组合时,要注意 reactive
属性必须用 getter 函数;深度监听和立即执行要根据场景灵活配置;实际开发中结合“表单验证、购物车计算、多条件搜索”等场景,能高效实现复杂逻辑,同时避开“语法错误、性能滥用、内存泄漏”等坑,才能让多源 watch 用得顺手又高效~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。