Vue3中如何在不启用deep监听且不监听所有嵌套层级的前提下,监听数组结构变化?
我之前也遇到过这个一模一样的问题!Vue2里数组用push/pop这类变异方法时,watch会自动触发,但Vue3换成Proxy之后,默认的数组watch只盯数组的引用变化,所以直接push/pop根本不会触发。而且deep:true又会因为递归监听所有嵌套属性导致页面卡成狗,确实头疼。
先给你捋清楚根源:Vue2对数组的7个变异方法(push、pop、shift、unshift、splice、sort、reverse)做了专门的拦截包装,所以这些操作会主动触发数组的watch;但Vue3用了原生Proxy,不会额外做这个拦截,所以调用push时数组引用没变,默认的watch(只检测引用)就毫无反应。而deep:true会遍历数组里每个元素的所有嵌套属性,你的元素属性多的话,性能爆炸是必然的。
下面给你几个实用的解决方案,按推荐程度排序:
方案1:在修改数组的方法里直接触发逻辑(最推荐,性能最优)
既然你的增删操作都是通过addItem、removeItem这类统一的方法触发的,那完全可以把原来watch里的逻辑抽成单独方法,在修改数组之后直接调用。这样既不需要watch,也没有额外的监听开销,性能拉满。
修改后的完整代码:
<template> <button @click="addItem">Add</button> <button @click="removeItem">Remove</button> <br><br> <div>Items:</div> <div v-for="item in items">{{item}}</div> </template> <script> export default { data() { return { items: [], }; }, methods: { addItem() { this.items.push({i: this.items.length}); this.onItemsStructureChange(); // 直接触发逻辑 }, removeItem() { this.items.pop(); this.onItemsStructureChange(); // 直接触发逻辑 }, onItemsStructureChange() { console.log('items', this.items); // 这里放你原来watch里的所有业务逻辑 } } }; </script>
这个方案的好处是完全可控,只有你明确修改数组结构时才触发逻辑,没有任何多余的监听开销。唯一的小约束是如果有其他地方直接修改数组(比如调用this.items.splice),要记得手动调用这个方法,但只要是你自己维护的业务代码,这点完全可以接受。
方案2:用watch监听数组元素的引用集合(适合修改入口分散的场景)
如果你的数组修改入口比较散,没法统一在方法里调用逻辑,可以用Vue3的watch监听一个自定义的getter源:返回数组元素引用组成的集合。这样只有当数组的元素增删(结构变化)时,getter返回的内容才会变化,watch就会触发;而元素内部的属性变化时,元素引用没变,watch不会触发,完美匹配你的需求。
修改后的script部分:
<script> export default { data() { return { items: [], }; }, watch: { // 监听自定义getter:返回数组元素的引用集合 'items.map(item => item)': { handler() { console.log('items', this.items); }, deep: false // 不需要deep,我们只盯元素引用的变化 } }, methods: { addItem() { this.items.push({i: this.items.length}); }, removeItem() { this.items.pop(); } } }; </script>
原理很简单:当你增删数组元素时,items.map(item => item)会返回一个元素引用顺序/数量不同的数组,Vue的watch会检测到这个变化并触发handler;而当你修改元素内部属性(比如item.i = 100),元素的引用没变,map返回的数组和之前的内容完全一致,watch就不会触发。
方案3:用effect追踪数组元素引用(Composition API风格,Options API也兼容)
如果你用Composition API的话,可以用effect手动追踪依赖:只要在effect里访问数组的每个元素引用,当数组结构变化时,effect就会触发;元素内部变化则不会触发。Options API里也可以通过手动导入effect来使用。
Options API的示例:
<script> import { effect } from 'vue'; export default { data() { return { items: [], }; }, mounted() { // 创建effect,在里面访问数组的每个元素引用 this.itemsEffect = effect(() => { // 遍历数组,让响应式系统追踪元素引用的变化 this.items.forEach(item => item); // 这里放你的业务逻辑 console.log('items', this.items); }); }, beforeUnmount() { // 记得销毁effect,避免内存泄漏 this.itemsEffect && this.itemsEffect(); }, methods: { addItem() { this.items.push({i: this.items.length}); }, removeItem() { this.items.pop(); } } }; </script>
这个方案和方案2原理类似,都是只追踪数组元素的引用变化,不追踪内部属性,性能很好。不过Options API里用effect需要手动管理销毁,稍微麻烦一点,适合一些特殊场景。
最后提一句你之前试的this.items = [...this.items, newItem]:其实这个方法的性能并没有你想象的那么差,Vue的虚拟DOM diff会复用已有的节点,创建新数组的开销其实很小;但如果数组元素特别多(比如上万条),那确实不如前面的方案优雅。
备注:内容来源于stack exchange,提问作者Kel Varnsen

