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前端网发表,如需转载,请注明页面地址。
code前端网




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