Vue3里怎么watch数组?要注意哪些点?
不少用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
后,watch
的oldVal
和newVal
可能“指向同一个引用”,比如上面的例子,改了对象属性后,newVal
和oldVal
都是原来的数组引用(因为数组本身没换,只是内部对象变了)。
这时候想拿到“修改前的旧值”,得自己手动处理(比如在watch
外存一份深拷贝的旧值,每次触发时更新)。deep
会遍历数组所有层级,数据量大时性能很差,所以尽量少用deep
,改成“局部精准监听”:
- 比如数组里是对象,只监听对象的某个属性变化;
- 或者在修改对象时主动替换引用(比如
arr.value[0] = { ...arr.value[0], name: '张四' }
),让数组引用变化,普通watch
也能触发。
watch数组时新旧值为啥有时候一样?
很多同学疑惑:“我明明改了数组,咋newVal
和oldVal
打印出来一模一样?” 这和JS的引用类型特性有关。
新旧值相同的两种场景
Vue3的watch
默认是“浅监听”,只有当数组的引用变化(比如替换整个数组:arr.value = [1,2,4]
),或者数组的长度/元素引用变化(比如push
一个新对象,新对象是新引用)时,oldVal
和newVal
才会不同。
但如果是下面两种情况,新旧值会“看起来一样”:
- 用“非响应式方法”改数组元素(比如
arr.value[0] = 10
):这时候数组引用没换,Vue没检测到变化,watch
根本不触发,自然也不存在新旧值; - 开了
deep
监听,且只改了数组内部对象/数组的属性(比如改对象name
、改嵌套数组元素):这时候数组本身的引用没换,所以oldVal
和newVal
指向同一个数组,打印出来就“一样”。
解决新旧值相同的思路
- 想让
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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。