Code前端首页关于Code前端联系我们

Vue3里怎么watch props?要注意哪些点?

terry 2小时前 阅读数 8 #Vue
文章标签 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属性

如果要同时监听userListpage两个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拿oldValnewVal
  • 用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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门