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

一、Vue3 watch 监听多个源的基础语法

terry 2小时前 阅读数 5 #Vue

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 里数据源分 refreactivecomputed(计算属性)等,组合监听时要注意各自特性:

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 })

初始化时,keywordcategory 的初始值会被当作 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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门