Vue3里怎么watch props?要注意哪些点?
在Vue3开发中,不少同学做组件通信时,碰到父组件把数据传给子组件后,想在子组件里“盯住”props的变化做逻辑,却搞不清怎么用watch正确实现,比如子组件接收了父组件的商品列表`goodsList`,想在列表变化时重新渲染图表;或者父组件切换筛选条件,子组件要跟着刷新数据请求,这时候咋用watch监听props?监听过程又有哪些容易踩的坑?今天把Vue3 watch props的门道拆明白。
为啥子组件要watch props?
先想清楚场景:父组件的数据变化会触发子组件props更新,子组件得“响应”这种变化执行逻辑,常见场景比如:
- 数据驱动视图更新:父组件切换Tab,子组件接收Tab标识,变化时重新拉取对应Tab的接口数据;
- 配置项变化重置状态:父组件传给子组件的
config
对象变了(比如分页大小从10改成20),子组件得重置分页器、重新请求数据; - 衍生数据计算:props是原始列表,子组件要根据列表变化,重新计算过滤后的列表、统计总数等。
要是不监听props,子组件内部逻辑就没法感知父组件传来的数据变化,要么页面不更新,要么得写重复代码在父组件里触发,代码耦合度超高。
Vue3里咋用watch监听props?
Vue3的watch
需要配合setup
语法(或语法糖)使用,核心是用getter函数指定要监听的props属性,再写回调处理变化,分几种常见情况讲:
监听单个props属性(基础用法)
子组件先通过defineProps
声明接收的props,再用watch
监听,举个例子,子组件接收父组件的userList
,变化时打印日志:
<script setup> import { watch } from 'vue' // 声明接收的props const props = defineProps(['userList']) // 监听userList变化 watch( () => props.userList, // getter函数,返回要监听的props属性 (newVal, oldVal) => { // 变化时的回调,newVal是新值,oldVal是旧值 console.log('用户列表变了:', newVal, oldVal) // 这里写业务逻辑,比如重新请求接口、更新图表等 } ) </script>
⚠️ 注意:不能直接写watch(props.userList, ...)
!因为props.userList
如果是字符串、数字这类基础类型,直接传的话,watch拿到的是初始值的“快照”,后续props变化时无法触发监听,必须用getter函数(箭头函数返回props.xxx
),让Vue能跟踪响应式依赖。
监听多个props属性
如果要同时监听userList
和page
两个props,用数组包裹多个getter函数:
watch( [() => props.userList, () => props.page], ([newList, newPage], [oldList, oldPage]) => { // 当userList或page变化时,回调执行 console.log('列表或页码变了:', newList, newPage, oldList, oldPage) } )
数组里的每个getter对应一个props属性,回调的参数也是数组,按顺序对应新值和旧值。
监听整个props对象(谨慎使用)
如果非要监听所有props的变化(不推荐,性能差),可以直接传props
给watch:
watch( props, (newProps, oldProps) => { console.log('props里某个属性变了:', newProps, oldProps) } )
但这么做会监听props
里所有属性的变化,哪怕只是一个无关紧要的属性修改,都会触发回调,除非你明确需要“任何props变化都执行逻辑”,否则尽量监听具体属性,减少不必要的性能消耗。
watch props时容易踩的坑
很多同学照着代码写,结果监听不到变化、重复触发或者性能爆炸,大概率是踩了这些坑:
getter函数写错,监听不到变化
最常见的错误是没写getter函数,直接传props.xxx
。
// 错误写法!监听不到变化 watch(props.userList, (newVal) => { ... })
因为props.userList
如果是基础类型(如字符串),watch一开始拿到的是初始值的拷贝,后续props更新时,这个拷贝不会自动同步,所以监听不到。必须用() => props.xxx
这种getter形式,让Vue能跟踪props.xxx
的响应式变化。
引用类型(对象/数组)的深层监听问题
如果props是对象或数组(引用类型),只改内部属性,引用没变化,watch默认不触发,比如父组件传{ name: '小明' }
,后来父组件只改name
为'小红'
,子组件这样写监听不到:
// 父组件没替换整个对象,只是改了name,props.obj的引用没变 watch(() => props.obj, (newVal) => { ... }) // 不会触发
这时候有两种解决办法:
- 让父组件修改时替换整个对象(推荐):父组件把
obj
改成{ ...obj, name: '小红' }
,这样引用变化,子组件watch能触发; - 子组件开启deep监听:给watch加
{ deep: true }
选项,但性能开销大,谨慎用:watch(() => props.obj, (newVal) => { ... }, { deep: true })
初始化时是否执行回调?
watch默认是惰性的——组件初始化时不执行回调,只有props变化时才执行,如果需要初始化时就执行一次(比如第一次拿到props就请求数据),加immediate: true
:
watch(() => props.userList, (newVal) => { // 初始化时执行,之后userList变化也执行 console.log('初始化或变化时执行:', newVal) }, { immediate: true })
watch和watchEffect,监听props选哪个?
Vue3里还有个watchEffect
,和watch
功能类似但用法不同,简单说:
- watch:需要明确指定监听的数据源(比如props.xxx),能拿到新旧值,默认惰性执行(可通过
immediate
修改); - watchEffect:自动收集回调里用到的响应式数据(比如props.xxx)作为依赖,拿不到旧值,初始化时就会执行一次(非惰性)。
举个场景对比:
- 用watch:子组件需要对比props的新旧列表,找出新增元素,这时候必须用watch拿
oldVal
和newVal
; - 用watchEffect:子组件只要props.list变化,就重新计算“已完成任务数”,不需要旧值,用watchEffect更简洁:
watchEffect(() => { const doneCount = props.list.filter(item => item.done).length console.log('已完成任务数:', doneCount) })
实际项目的最佳实践
光会用还不够,得结合项目场景写高效、易维护的代码:
精准监听,避免性能浪费
- 别监听整个props对象,尽量监听具体属性(如
() => props.userList
); - 引用类型优先让父组件替换整个对象(改引用),少用
deep
监听; - 如果要监听对象内部属性,直接写
() => props.obj.name
,比deep
监听整个obj
高效。
结合computed处理衍生数据
如果props变化后,需要先做数据转换(比如过滤、排序),再执行逻辑,建议用computed
先处理数据,再watch computed结果:
<script setup> import { watch, computed } from 'vue' const props = defineProps(['rawList']) // 先用computed过滤出已完成的任务 const doneList = computed(() => props.rawList.filter(item => item.done)) // 再watch过滤后的列表 watch(doneList, (newDoneList) => { console.log('已完成列表变化:', newDoneList) // 基于doneList做渲染、请求等逻辑 }) </script>
这样分工明确:computed
负责数据转换,watch
负责响应变化,代码更清晰。
别过度依赖watch,试试其他通信方式
有时候props变化后要触发子组件方法,不一定非用watch,比如父组件通过ref
调用子组件暴露的方法:
<!-- 子组件 --> <script setup> const handleRefresh = () => { // 刷新逻辑 } defineExpose({ handleRefresh }) </script> <!-- 父组件 --> <template> <Child ref="childRef" :list="list" /> </template> <script setup> const childRef = ref() const changeList = () => { list.value = [...newList] childRef.value.handleRefresh() // 父组件主动调用子组件方法 } </script>
这种方式适合父组件主动控制子组件逻辑的场景,和watch(子组件被动响应props)互补。
Vue3里watch props的核心是用getter函数指定监听目标,避开“没写getter、深层监听滥用、初始化执行”这些坑,再结合computed、watchEffect等工具优化逻辑,只要把这些细节吃透,不管是简单的列表更新,还是复杂的配置项响应,都能优雅处理~要是你还有其他关于Vue3组件通信的疑问,评论区随时聊~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。