×

Vue3中watch监听props时为什么要用deep?怎么正确使用?

提问者:Terry2025.05.06浏览:220

Vue3中watch监听props时为什么要用deep?怎么正确使用?

很多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人参与

发表评论: