Vue3 中如何正确watch监听props传来的值?深监听要注意什么坑?
刚接触Vue3开发的小伙伴,大概率踩过props监听的坑:明明父组件传过来的值在控制台打印变了,watch却纹丝不动;或者随便加了个deep:true,页面卡得像PPT;还有Options API和Composition API混用的时候,写法容易搞混,别慌,今天咱们就把Vue3监听props的全流程讲清楚,从基础逻辑到避坑技巧,再到不同API的选择,看完就能彻底搞定这个问题。
为什么你写的watch props经常失效?
先得搞懂Vue3的响应式原理,特别是针对props的处理逻辑,不然踩坑都不知道踩在哪。 Vue3的props本身是只读的,这是为了避免子组件直接修改父组件数据,导致数据流混乱——但只读不代表不能监听变化。 Vue3监听数据的核心是拦截属性访问或修改,如果数据是基础类型(比如字符串、数字、布尔值),父组件每次传新值,相当于给props这个变量重新赋值,自然会触发拦截器,普通watch就能监听到;但如果是引用类型(对象、数组),父组件只修改了内部的属性/元素,props本身的引用地址没变,普通watch的拦截器只会看引用地址,当然不会有反应。
举个最直观的例子:父组件有个todo对象,const todo = ref({ id:1, text:'写代码' }),传给子组件的props叫currentTodo,子组件里写watch(props.currentTodo, (newVal) => console.log(newVal)),如果父组件只改todo.value.text = '摸鱼',子组件的todo变量引用的还是堆里同一个对象,拦截器没检测到整体赋值,所以watch不会打印;但如果父组件写todo.value = { id:1, text:'摸鱼' },这是重新创建了一个对象赋值给ref,引用地址变了,普通watch就能正常工作。
Options API 和 Composition API 下的监听写法有啥不同?
Vue3现在支持两种API风格,很多人习惯了Options,转Composition的时候写法容易写错,这里分开讲清楚。
Options API:熟悉的配方,微调的细节
Options API是Vue2就有的写法,在Vue3里基本没变,但有个小细节要注意:props声明可以更规范,加类型约束后监听更稳。
监听基础类型props
直接在watch选项里写props属性名: 回调函数就行,或者写成对象形式加配置项,
export default {
props: {
count: {
type: Number,
required: true
}
},
watch: {
// 简写形式,只监听值变化
count(newVal, oldVal) {
console.log('count变了:', newVal, oldVal)
},
// 对象形式,可以加immediate、deep这些配置
count: {
handler(newVal, oldVal) {
console.log('count变了(配置版):', newVal, oldVal)
},
immediate: true, // 组件挂载后立刻执行一次回调
deep: false // 基础类型默认不用开
}
}
}
监听引用类型props的整体/内部变化
如果父组件只会重新赋值整个引用类型(比如用Vuex/pinia获取的新列表直接替换),普通写法就行;如果父组件只改内部属性/元素,必须加deep:true,
export default {
props: {
todoList: {
type: Array,
required: true,
// 还可以加自定义验证,比如不能为空
validator: (val) => val.length > 0
},
userInfo: {
type: Object,
required: true
}
},
watch: {
// 监听todoList的整体替换
todoList(newVal) {
console.log('todoList整体换了')
},
// 监听todoList内部元素的增删改、userInfo内部属性的变化
'todoList': {
handler(newVal) {
console.log('todoList内部变了')
},
deep: true
},
// 也可以只监听引用类型的某个具体属性,不需要deep,性能更好!
'userInfo.name': {
handler(newVal) {
console.log('用户名变了:', newVal)
},
immediate: true
}
}
}
Composition API:推荐的写法,但要注意this和响应式源
Composition API是Vue3的核心特性,代码更灵活、复用性更高,但监听props的响应式源要选对,不能直接写props.currentTodo的原始值,不然会失去响应式。
监听基础类型props
在<script setup>语法糖里,直接用watch()函数,第一个参数传props的属性名或者返回props属性的函数(推荐后者,更稳,特别是组合式函数里用的时候),
<script setup>
import { watch } from 'vue'
// 声明props
const props = defineProps({
count: Number
})
// 写法1:直接传props属性(语法糖里可以,但组合式函数里可能失效)
watch(props.count, (newVal, oldVal) => {
console.log('count变了(直接传):', newVal, oldVal)
})
// 写法2:传返回props属性的箭头函数(通用写法,永远有效)
watch(() => props.count, (newVal, oldVal) => {
console.log('count变了(函数形式):', newVal, oldVal)
}, {
immediate: true
})
</script>
为什么推荐箭头函数?因为在组合式函数(hooks)里,我们可能会把props作为参数传进去,直接写参数的属性可能会被Vue的响应式系统“忽略”,但箭头函数会每次调用时重新获取props的属性,从而建立正确的依赖追踪。
监听引用类型props的整体/内部/具体属性变化
同样的,箭头函数形式通用:
<script setup>
import { watch } from 'vue'
const props = defineProps({
todoList: Array,
userInfo: Object
})
// 监听整体替换
watch(() => props.todoList, (newVal) => {
console.log('todoList整体换了')
})
// 监听内部变化,加deep:true
watch(() => props.todoList, (newVal) => {
console.log('todoList内部变了')
}, {
deep: true
})
// 只监听具体属性,性能最优
watch(() => props.userInfo?.age, (newVal) => { // 加可选链防止props未初始化报错
console.log('用户年龄变了:', newVal)
}, {
immediate: true,
flush: 'post' // 等DOM更新后再执行回调,比如需要获取元素尺寸的时候
})
</script>
这里还要提一下flush配置项,默认是'pre',也就是在DOM更新前执行回调;'post'是DOM更新后;'sync'是同步触发,不推荐用,可能会影响性能。
深监听(deep:true)有什么不能踩的坑?
很多人发现普通watch引用类型失效,直接就加个deep:true,这是最省事的,但也是最容易出问题的,主要有三个大坑:
坑1:性能消耗大,大数据量时页面卡顿
deep:true会递归遍历引用类型的所有层级,不管是嵌套了10层的对象还是1000条数据的数组,每次内部有一点点变化(比如数组里某个对象的布尔值从true变false),都会触发整个遍历,性能消耗呈指数级增长。
如果你的数据量不大(比如小于100条),嵌套层级不深(比如1-2层),深监听没问题;但如果数据量很大或者嵌套很深,尽量只监听具体属性,或者让父组件只在需要的时候重新赋值整个引用类型,或者用watchEffect结合computed优化?不对,computed结合watch也行,比如只取你需要的那部分数据computed出来,再监听这个computed值,性能会好很多。
坑2:oldVal和newVal一样,区分不了变化前后的内容
因为引用类型的引用地址没变,deep:true回调里的oldVal和newVal指向的是堆里同一个对象,所以打印出来的内容是一样的,没法直接对比变化前后的差异。
怎么解决?如果一定要对比,可以用lodash的cloneDeep先深拷贝一份旧值,或者用watchEffect结合状态管理?或者自己写一个浅拷贝/深拷贝的逻辑,只拷贝你需要对比的部分。
<script setup>
import { watch, ref } from 'vue'
const props = defineProps({ userInfo: Object })
// 先存一份初始的深拷贝(用JSON方法也行,但不能处理函数、Symbol等特殊类型)
const oldUserInfo = ref(JSON.parse(JSON.stringify(props.userInfo)))
watch(() => props.userInfo, (newVal) => {
console.log('旧的用户名:', oldUserInfo.value.name)
console.log('新的用户名:', newVal.name)
// 记得更新旧值,下次对比才对
oldUserInfo.value = JSON.parse(JSON.stringify(newVal))
}, { deep: true })
</script>
但深拷贝本身也有性能消耗,还是那句话,尽量少用。
坑3:监听的是props的属性,不是props本身,但不小心加了deep
有些新手会直接写watch(props, ..., { deep:true }),这其实没必要,而且会监听所有props的变化,不管是不是你需要的,增加了不必要的性能消耗。
尽量只监听你真正需要的props属性,或者具体属性。
watch和watchEffect怎么选?监听props用哪个更合适?
很多人分不清这两个API的区别,
- watch是惰性的(默认,除非加immediate),只监听你指定的响应式源,回调里能拿到newVal和oldVal,适合需要明确知道变化前后内容的场景;
- watchEffect是立即执行的,自动追踪回调里用到的所有响应式数据,没法直接拿到oldVal,适合只要数据变了就执行某个操作,不需要知道变化前后内容的场景。
那监听props用哪个?大部分场景用watch更合适,因为:
- 监听props通常是为了处理某个具体属性的变化,不需要追踪所有;
- 有时候需要知道变化前后的内容做逻辑判断;
- 惰性执行可以避免组件挂载时不必要的逻辑(除非加immediate)。
但也有例外,比如你要处理多个props的变化,而且只要其中一个变了就执行同一个操作,用watchEffect会更简洁:
<script setup>
import { watchEffect } from 'vue'
const props = defineProps({ userId: Number, page: Number })
// 自动追踪userId和page,只要其中一个变了就请求数据
watchEffect(() => {
if (props.userId && props.page) {
fetchData(props.userId, props.page)
}
})
function fetchData(userId, page) {
// 这里写请求逻辑
console.log('请求数据:userId=', userId, 'page=', page)
}
</script>
有没有完全替代深监听的方法?
既然深监听有这么多坑,有没有更好的方法?有三个常用的:
方法1:父组件只传基础类型,或者扁平化引用类型
把嵌套的对象/数组拆成多个基础类型的props,比如把userInfo拆成userName、userAge、userGender三个props,这样只需要监听这三个基础类型,不用深监听,性能最好。
方法2:父组件在修改内部属性/元素时,重新赋值整个引用类型
比如用todoList.value = [...todoList.value, newTodo]代替todoList.value.push(newTodo),用userInfo.value = { ...userInfo.value, name: '新名字' }代替userInfo.value.name = '新名字',这样普通watch就能监听到,不需要deep。
方法3:用computed计算你需要的那部分数据,再监听computed值
比如你只需要监听todoList里的未完成数量,就先computed一个unfinishedCount,再监听这个count,性能比深监听整个todoList好太多:
<script setup>
import { watch, computed } from 'vue'
const props = defineProps({ todoList: Array })
const unfinishedCount = computed(() => props.todoList.filter(todo => !todo.done).length)
watch(unfinishedCount, (newVal) => {
console.log('未完成数量变了:', newVal)
}, { immediate: true })
</script>
总结一下正确监听props的步骤
- 先看传过来的是基础类型还是引用类型:
- 基础类型:用普通watch(箭头函数形式通用);
- 引用类型:先看父组件的修改方式,是整体替换还是内部修改;
- 如果是整体替换,普通watch就行;
- 如果是内部修改,优先考虑父组件重新赋值、只监听具体属性、computed中间层这三种方法,实在不行再加
deep:true; - 根据是否需要oldVal、是否需要明确指定响应式源,选择watch或者watchEffect;
- 组合式函数里一定要用箭头函数形式传响应式源。
监听props是Vue3开发里最常用的功能之一,只要搞懂了响应式原理,避开深监听的坑,就能写出性能好、逻辑清晰的代码。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


