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

Vue3里怎么watch数组?要注意哪些点?

terry 13小时前 阅读数 12 #Vue

不少用Vue3做项目的同学,碰到数组监听时总会犯难——明明改了数组元素,watch咋没触发?数组里嵌套对象、数组的情况又该咋监听?今天就把Vue3 watch数组的门道掰开揉碎讲清楚,从基础用法到复杂场景都覆盖到~

Vue3 watch数组的基础用法是啥样?

先明确Vue3的watch语法:watch(源, 回调, 配置项),当要监听数组时,“源”的写法和数组的响应式创建方式有关,核心是让Vue能检测到数组的变化

ref定义数组的情况

如果用ref包裹数组(最推荐的数组响应式写法),watch的“源”可以直接传ref对象,Vue会自动解包,看例子:

import { ref, watch } from 'vue'
const arr = ref([1, 2, 3])
watch(arr, (newVal, oldVal) => {
  console.log('数组变了!新值:', newVal, '旧值:', oldVal)
})
// 用“响应式方法”修改数组,触发watch  
arr.value.push(4) // 触发watch,newVal是[1,2,3,4],oldVal是[1,2,3]  

但要注意!如果直接通过索引改数组(比如arr.value[0] = 10),这种操作不会触发响应式更新,自然也不会触发watch,这时候得用“响应式修改技巧”:

  • 用数组变异方法(push/pop/splice等,这些方法会触发响应式更新);
  • Vue.set(Vue3中导入import { set } from 'vue',然后set(arr.value, 0, 10));
  • 直接替换数组(比如arr.value = [...arr.value.slice(0,0), 10, ...arr.value.slice(1)],让数组引用变化)。

reactive定义数组的情况

如果用reactive包裹对象,对象里包含数组(比如const state = reactive({ list: [1,2,3] })),watch的“源”必须写成函数形式,否则会失去响应性:

watch(() => state.list, (newVal, oldVal) => { 
  // 监听state.list的变化
}, { deep: false })

这种情况下,修改数组的逻辑和ref一致——必须用变异方法或替换引用,才能让watch感知到变化。

数组里有对象/嵌套数组,咋深度监听?

如果数组里存的是对象(比如[{name: '张三'}, {name: '李四'}]),或者数组嵌套数组(比如[[1,2], [3,4]]),只监听数组本身的“长度变化、引用变化”就不够了。

比如改了对象的属性(arr[0].name = '张四'),或者改了嵌套数组里的元素(arr[0][0] = 10),普通watch是监听不到的,这时候得开deep: true配置项,让watch“钻进去”监听深层变化。

深层监听的写法

看例子:

const arr = ref([{ name: '张三' }, { name: '李四' }])
// 普通监听:改对象属性不会触发
watch(arr, (newVal, oldVal) => {
  console.log('普通监听,改对象属性不触发')
}, { deep: false }) 
// 深层监听:改对象属性会触发
watch(arr, (newVal, oldVal) => {
  console.log('深层监听,改对象属性触发')
}, { deep: true })
// 测试:修改对象属性
arr.value[0].name = '张四' // 只有开了deep的watch会触发

深层监听的“坑”

开启deep后,watcholdValnewVal可能“指向同一个引用”,比如上面的例子,改了对象属性后,newValoldVal都是原来的数组引用(因为数组本身没换,只是内部对象变了)。

这时候想拿到“修改前的旧值”,得自己手动处理(比如在watch外存一份深拷贝的旧值,每次触发时更新)。deep会遍历数组所有层级,数据量大时性能很差,所以尽量少用deep,改成“局部精准监听”:

  • 比如数组里是对象,只监听对象的某个属性变化;
  • 或者在修改对象时主动替换引用(比如arr.value[0] = { ...arr.value[0], name: '张四' }),让数组引用变化,普通watch也能触发。

watch数组时新旧值为啥有时候一样?

很多同学疑惑:“我明明改了数组,咋newValoldVal打印出来一模一样?” 这和JS的引用类型特性有关。

新旧值相同的两种场景

Vue3的watch默认是“浅监听”,只有当数组的引用变化(比如替换整个数组:arr.value = [1,2,4]),或者数组的长度/元素引用变化(比如push一个新对象,新对象是新引用)时,oldValnewVal才会不同。

但如果是下面两种情况,新旧值会“看起来一样”:

  1. 用“非响应式方法”改数组元素(比如arr.value[0] = 10):这时候数组引用没换,Vue没检测到变化,watch根本不触发,自然也不存在新旧值;
  2. 开了deep监听,且只改了数组内部对象/数组的属性(比如改对象name、改嵌套数组元素):这时候数组本身的引用没换,所以oldValnewVal指向同一个数组,打印出来就“一样”。

解决新旧值相同的思路

  • 想让watch触发+拿到不同新旧值:修改数组时用“替换引用”的方式(比如arr.value = [...arr.value.slice(0,0), 10, ...arr.value.slice(1)]),或者用变异方法(push/pop等);
  • 开了deep后想区分新旧值:在watch触发前,手动深拷贝旧值(比如在watch外维护一个拷贝,每次watch触发前更新旧值拷贝)。

实际项目里watch数组的常见场景有哪些?

理解了基础逻辑,得结合业务场景才好落地,这些场景你肯定遇过:

场景1:表单多选值的同步

比如表单里有多个复选框,选中项用数组checkedKeys存,当checkedKeys变化时,要同步更新“提交按钮是否可用”“已选数量提示”等状态。

const checkedKeys = ref([])
watch(checkedKeys, (newVal) => {
  // 控制提交按钮状态:至少选一个才可用
  submitBtnDisabled.value = newVal.length === 0
  // 显示已选数量
  selectedCount.value = newVal.length
})

场景2:列表数据过滤/计算

比如后台返回的原始列表rawList,前端根据搜索关键词searchKey过滤出filteredList,但如果rawList是异步获取的(比如分页加载后数组变长),这时候要监听rawList的变化,重新执行过滤逻辑。

const rawList = ref([])
const filteredList = ref([])
watch(rawList, (newList) => {
  filteredList.value = newList.filter(item => item.name.includes(searchKey.value))
}, { immediate: true }) // 组件加载时先执行一次

场景3:多数组联动逻辑

比如页面有“已选商品数组”和“优惠规则数组”,当其中任意一个数组变化时,要重新计算“最终优惠金额”,这时候可以把两个数组都放进watch的源里:

const selectedGoods = ref([])
const discountRules = ref([])
watch([selectedGoods, discountRules], ([newGoods, newRules]) => {
  // 结合两个数组计算优惠
  calcDiscount(newGoods, newRules)
})

额外避坑:响应式数组的创建方式影响watch

最后补个容易踩的坑:ref还是reactive定义数组,会影响watch的写法。

  • ref定义数组:const arr = ref([1,2,3])watch的源可以是arr(自动解包),也可以是() => arr.value
  • reactive定义数组(不推荐,因为reactive对数组的响应式支持不如ref直观):const state = reactive({ list: [1,2,3] })watch的源必须是函数式:() => state.list,否则直接传state.list会失去响应性(因为state.list是数组引用,一旦传值过去,watch就只监听这个引用,后续数组引用变化也监听不到了)。

Vue3 watch数组的核心是理解响应式更新规则watch的监听逻辑:用响应式方法改数组、合理开`deep`、处理引用类型的新旧值问题,再结合业务场景设计监听逻辑,就能避开绝大多数坑~ 下次碰到数组监听没触发,先检查是不是修改方式不对,再看配置项和引用问题,基本能解决~

版权声明

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

发表评论:

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

热门