Vue3 watch监听props数据没反应怎么办?常见原因和解决方案全梳理
最近在做Vue3项目重构或者写新组件的时候,你有没有碰到这种挠头的情况:父组件明明通过props传了数据,子组件也写了watch监听,可要么父组件改了数据子组件完全没动静,要么只在初始化时触发一次后面就失效了?别慌,这事儿在Vue3新手上手时太常见了,我整理了自己踩过的和项目里见过的所有坑,连官网更新不久的Suspense相关的边界情况都没落下,给你一份实用到能当笔记抄的解决方案。
先明确前提:props的本质和Vue3 watch的特性
在找坑之前,得先把基础逻辑理清楚,不然解决了一个问题,换个场景又踩同一个,props在Vue里是单向数据流,父组件传给子组件的都是“只读引用”或者“只读值”——注意哦,这里的只读不是完全不能碰,而是碰了Vue会警告,而且如果是对象/数组这类引用类型,直接改子组件的props会影响父组件,但不建议这么做,Vue3的watch和Vue2相比有几个大变化,也是很多坑的根源:比如默认不深度监听引用类型的内部属性变化(浅监听)、初始化默认不执行、支持了监听多个源但要注意格式、ref和reactive的监听方式不一样,还有用了setup语法糖的script setup里,props的解构如果不当也会出问题。
浅监听,引用类型的外层地址没变内部变了
这应该是新手最容易犯的第一个错误,举个例子,父组件传了一个对象userInfo给子组件,父组件每次只改userInfo.name或者userInfo.age,子组件用了watch(props.userInfo, (newVal) => {console.log(newVal)})——这时候肯定没反应,因为浅监听只会看引用类型的指针地址,指针没动(也就是没给userInfo重新赋值一个新对象),Vue3就认为它没变化。
怎么解决? 有三个常用的方法,按需选就行。
第一个是加deep: true配置项,在watch的第三个参数对象里加上这个,不管引用类型嵌套多少层,内部属性变了都会触发,不过要注意,深度监听的性能开销会比浅监听大,尤其是嵌套非常深、数据量很大的对象/数组,要谨慎用,如果只需要监听内部某一个具体的属性,比如userInfo.name,用第三个方法会更好。
第二个是父组件每次改的时候都重新赋值一个新对象/数组,比如父组件原来写this.userInfo.name = '张三'(Vue2的写法)或者userInfo.value.name = '张三'(Vue3的ref写法),改成this.userInfo = {...this.userInfo, name: '张三'}或者userInfo.value = {...userInfo.value, name: '张三'},数组的话就用[...arr, newValue]这种展开语法,或者用map、filter这些返回新数组的方法,这个方法的好处是不用在子组件加deep,性能更好,而且符合Vue的单向数据流理念(虽然没强制,但官方推荐如果子组件想改引用类型的props,最好让父组件处理)。
第三个是在watch的第一个参数里直接写一个函数返回需要监听的具体属性,比如子组件可以写watch(() => props.userInfo.name, (newVal) => {console.log(newVal)}),或者监听数组的某一项也行,这种方法的性能是最好的,因为它只追踪具体那一个属性的变化,不会遍历整个引用类型。
script setup里用了普通解构破坏了响应式
如果你用了Vue3的script setup语法糖(这应该是现在写Vue3最常用的方式了吧),很多人为了写代码方便,会直接const { name, age } = defineProps(['userInfo']),然后监听name或者age——这时候大概率又会没反应,为啥?因为普通解构会把props里的响应式值变成普通的JavaScript变量,失去了响应式特性。
怎么解决? 同样有三个方法,看你习惯哪种。
第一个是不解构,直接用props.userInfo.name来取值和监听,这是最稳妥的方式,完全不会出错,虽然代码多写了几个字,但对新手来说很友好。
第二个是用Vue3提供的toRefs或者toRef来保持响应式。toRefs会把整个props对象里的所有属性都变成ref,比如const props = defineProps(['userInfo']); const { userInfo } = toRefs(props),这时候监听userInfo.value.name或者userInfo都可以,不过要注意哦,如果props里有可选属性(比如userInfo可能没传,初始值是undefined),用toRefs可能会导致解构出来的ref初始值是undefined,而且以后父组件不传的话也不会有默认值(除非你在defineProps里加了默认值),这时候可以用toRef,比如const userInfo = toRef(props, 'userInfo'),toRef专门用来处理单个属性,而且如果props里的属性不存在,会创建一个响应式的ref,初始值是你传的第三个参数,比如const userInfo = toRef(props, 'userInfo', {name: '默认名', age: 18})。
第三个是如果只需要解构出普通变量来显示(不需要修改也不需要监听),其实用普通解构也可以,但如果要监听或者绑定到v-model上(哦不对,v-model不能直接绑定props哦,除非用computed包装一层),还是要用前面的方法。
初始化没触发,或者没设置immediate
有时候你可能需要子组件一挂载或者一接收props就触发一次watch回调,比如根据父组件传的初始ID去请求接口获取详情数据,但默认情况下Vue3的watch只会在监听的源变化时才触发,初始化的时候是不会执行的——很多人会误以为这是watch无效,其实只是没加immediate配置项。
怎么解决? 在watch的第三个参数对象里加上immediate: true就行啦,比如watch(() => props.userId, (newVal) => {fetchUserDetail(newVal)}, {immediate: true}),这时候不管父组件传的userId有没有变化,子组件一接收props(或者说watch一注册)就会先执行一次回调函数。
监听的是computed或者ref但格式不对?或者监听多个源的写法错了?
先讲监听computed和ref的坑:如果父组件传的props是一个computed,或者子组件自己把props用computed包装了一层,那watch的第一个参数直接写computed变量就行,不用加.value,因为computed本身就是一个响应式源;同理,如果是用defineProps解构出来的toRef/ref,直接写变量名,不用加.value,watch会自动解包的——不过如果是在回调函数里或者模板外面的其他地方用,还是要加.value哦,很多人会搞混这一点。
再讲监听多个源的坑:Vue3支持同时监听多个源,比如同时监听props.userId和props.pageSize,这时候第一个参数要写成一个数组,比如watch([() => props.userId, () => props.pageSize], ([newUserId, newPageSize], [oldUserId, oldPageSize]) => {console.log(newUserId, newPageSize)}),回调函数的第一个参数是新值的数组,第二个是旧值的数组,如果写成两个单独的watch当然也可以,但有时候一起监听更方便,比如两个源都变的时候才触发接口请求(这时候可以加flush: 'post'或者防抖节流,不过这是另外的话题了)。
边界情况:Suspense、异步组件、props初始值是Promise
Vue3新增了Suspense和异步组件,这两个功能有时候也会导致watch props无效或者延迟触发,比如父组件里用了Suspense包裹子组件,子组件的setup是async的,那子组件的watch会在setup执行完、Suspense resolved之后才会注册并可能触发;如果props初始值是一个Promise,那你直接监听这个Promise本身肯定没用,因为Promise的状态变化不会被Vue的响应式系统追踪,这时候你应该在子组件里用async/await或者.then()处理这个Promise,然后把结果存到一个ref/reactive里,再监听这个ref/reactive。
还有一个比较少见的坑:props是通过v-bind="$attrs"传的,而且没在defineProps里声明——这时候$attrs里的属性变化不会触发defineProps相关的监听,因为defineProps只追踪声明过的属性,如果你需要监听没声明的属性,可以用useAttrs()获取attrs对象,然后用watch(() => attrs.unDeclaredProp, (newVal) => {})来监听,不过要注意哦,useAttrs()返回的对象本身不是响应式的,但里面的属性如果是父组件传的响应式值,通过函数返回的方式监听是可以生效的。
给你一个快速排查的 checklist
以后碰到watch props无效的情况,别慌,按照下面的顺序一个个排查就行:
- 先看引用类型的话,是外层地址变了还是内部变了?内部变的话有没有加deep,或者有没有监听具体属性,或者父组件有没有重新赋值新对象/数组?
- 再看script setup里有没有用普通解构props?有的话有没有换成toRefs/toRef或者直接用props.xxx?
- 然后看有没有加immediate?如果需要初始化触发的话。
- 接着看监听的源格式对不对?computed/ref/toRef要不要加.value?监听多个源有没有写成数组?
- 最后看是不是边界情况?比如Suspense、异步组件、Promise props、v-bind="$attrs"的未声明属性?
只要把这些坑都避开,Vue3的watch props绝对是非常好用的工具。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



