很多Vue开发者在使用watch监听props时,会遇到这样的困惑:明明props里的对象属性变了,watch的回调却没触发,这时候总听人说要加deep选项,但具体为什么?怎么用才对?今天就来彻底解决这个问题。
为什么监听props对象/数组需要deep?
要理解deep的作用,得先回忆Vue3的响应式原理,Vue3用Proxy实现响应式,当我们访问对象的属性时,Proxy会记录这个“依赖”;当属性被修改时,会触发依赖的更新,但这里有个关键点:默认情况下,watch只监听被直接访问的属性的变化。
举个常见例子:父组件传递一个user对象作为props给子组件,结构是{ name: '张三', info: { age: 20 } }
,子组件用watch监听user:
watch(user, (newVal, oldVal) => { console.log('user变化了') })
这时候如果父组件修改user.name = '李四'
,watch会触发;但如果修改user.info.age = 21
,watch的回调大概率不会执行,因为Vue的响应式系统默认只追踪“顶层”属性的变化,对象内部的嵌套属性修改不会被watch直接捕获——这就是“浅监听”的局限性。
这时候就需要用到deep选项,开启deep后,watch会递归遍历被监听对象的所有属性,为每个属性建立依赖,当任意深层属性变化时,都会触发回调,简单说,deep的作用是让watch从“只看表面”变成“查户口式检查”。
Vue3中watch配合deep的正确写法
Vue3的watch有三种写法:监听ref、监听reactive对象、监听getter函数,针对props的监听,最常用的是后两种,搭配deep的方式也略有不同。
监听reactive类型的props(不推荐)
如果props本身是用reactive定义的对象(虽然这种情况较少,因为props通常是父组件传递的),直接监听时需要注意:
// 子组件接收props const props = defineProps({ user: { type: Object, required: true } }) // 错误写法(可能不触发) watch(props.user, (newVal, oldVal) => { console.log('user变化了') }, { deep: true })
这里的问题在于,props本身是响应式的,但props.user是一个普通对象(除非父组件用reactive包裹后传递),更准确的方式是监听整个props对象,或者用getter函数明确依赖。
推荐:用getter函数配合deep
Vue3官方更推荐通过getter函数来监听props的深层变化,这样可以避免直接监听整个props对象带来的性能问题,正确写法是:
watch( () => props.user, // getter函数返回要监听的值 (newUser, oldUser) => { console.log('user变化了', newUser.info.age) }, { deep: true } // 开启深层监听 )
这里的getter函数() => props.user
会返回当前的user对象,watch会追踪这个返回值的变化,开启deep后,无论user的哪个层级属性变化,都会触发回调。
监听数组的特殊情况
如果props是数组,比如list: [ { id: 1, text: 'a' }, { id: 2, text: 'b' } ]
,修改数组某个元素的属性(如list[0].text = 'aa'
),同样需要deep才能触发watch,写法和对象类似:
watch( () => props.list, (newList) => { console.log('列表元素变化了') }, { deep: true } )
使用deep时的常见误区和注意事项
虽然deep能解决深层监听的问题,但滥用会带来性能隐患,甚至导致意外的回调触发,这几个坑一定要避开:
不要用deep监听整个props对象
有些人为了省事,直接监听整个props:
// 不推荐! watch(props, (newProps, oldProps) => { console.log('props变化了') }, { deep: true })
这会导致props中任何一个属性(包括不相关的)变化时,都会触发回调,如果props包含大量数据或嵌套层级很深,每次变化都要递归遍历所有属性,会严重影响性能。正确做法是只监听需要的具体属性,比如() => props.user.info.age
。
oldVal可能和newVal相同
在深层监听对象时,Vue3的Proxy特性会导致oldVal和newVal可能指向同一个对象(因为对象是响应式的,修改后原对象被更新,而不是替换)。
watch( () => props.user, (newUser, oldUser) => { console.log(newUser === oldUser) // 可能输出true }, { deep: true } )
这是正常现象,因为Vue没有复制整个对象,而是直接修改原对象,如果需要对比变化前后的差异,建议手动深拷贝旧值,比如在回调开始时用JSON.parse(JSON.stringify(oldUser))
保存副本。
避免在watch中修改被监听的props
Vue明确规定“子组件不应直接修改props”,因为props是单向数据流,如果在deep的watch回调中修改props的深层属性(如newUser.info.age = 30
),虽然不会报错,但会导致父组件的数据被意外修改,引发双向绑定的混乱,正确做法是通过emit
通知父组件修改,或者在子组件内使用v-model
双向绑定(需要父组件配合)。
深层监听的替代方案:computed的妙用
如果只是需要获取深层属性的值并响应变化,其实不一定非要用watch+deep,computed配合watch的“浅监听”可能更高效。
需要监听user.info.age
的变化:
// 用computed提取深层属性 const userAge = computed(() => props.user.info?.age) // 监听computed返回的ref(浅监听即可) watch(userAge, (newAge, oldAge) => { console.log('年龄变化了', newAge) })
这种方式的好处是:computed会自动追踪依赖的深层属性(props.user.info.age
),当这个具体属性变化时,computed的值会更新,触发watch的回调,相比deep遍历整个对象,这种方式只追踪明确需要的属性,性能更好。
回到最初的问题:Vue3中watch监听props时用deep,本质是为了解决嵌套属性变化无法被“浅监听”捕获的问题,使用时要注意:
用getter函数明确监听目标,避免监听整个props;
深拷贝旧值以便对比变化;
避免直接修改props;
优先考虑computed替代方案,减少性能消耗。
掌握这些细节后,你就能在项目中灵活运用deep,既保证功能正确,又避免不必要的性能开销了。
网友回答文明上网理性发言 已有0人参与
发表评论: