Vue3组件怎么监听props数据变化?有几种高效方法?
我们在做Vue3项目的组件化开发时,经常会遇到子组件需要根据父组件传来的props值改变状态、触发函数的场景——比如父组件传过来一个筛选后的商品列表,子组件的分页页码要自动重置;或者传过来一个弹窗开关的布尔值,子组件要对应打开关闭动画,这时候,正确且高效地监听props变化就成了基础但关键的需求。
那Vue3到底有哪些监听props的方法?每种方法适合什么情况?有没有踩坑的地方?今天咱们就用日常开发的思路,把这些问题理清楚。
先明确Vue3 props的基本属性
讲监听方法前,先把props的规则说透,不然用起来容易出bug。 Vue3的props默认是单向数据流的:父组件改了props,子组件会自动更新视图,但子组件不能直接修改props的值,否则控制台会报错,这和Vue2是一样的,但Vue3的响应式底层从Object.defineProperty换成了Proxy,所以监听的底层逻辑变了,但对开发者的API调整不大,反而多了更清晰的选项式API和组合式API的选择。 props里的基础数据类型(比如字符串、数字、布尔值)是值传递,引用数据类型(比如数组、对象)是引用传递——这个点Vue2和3也一致,监听的时候要注意区分,不然可能出现“父组件改了数组元素,子组件没反应”或者“以为没直接改props,其实偷偷改了引用属性”的问题。
Vue3监听props的5种实用方法
组合式API用watch/watchEffect直接监听
这是Vue3里最常用、最灵活的方法了,尤其是做复杂业务逻辑的时候。 watch可以指定监听的具体props,还能配置是否立即执行、是否深度监听;watchEffect则是自动追踪依赖的响应式数据,包括props,不需要手动指定,适合依赖多个响应式数据联动的场景。 举个组合式API的例子,父组件传过来商品列表和当前页码的props:
<script setup>
// 引入响应式API
import { watch, watchEffect, ref } from 'vue'
// 定义要接收的props
const props = defineProps(['productList', 'currentPage'])
// 子组件自己的状态,比如缓存的上一页页码
const lastPage = ref(1)
// 用watch监听单个基础类型props:父组件改了currentPage就重置lastPage
watch(() => props.currentPage, (newVal, oldVal) => {
console.log(`页码从${oldVal}变成了${newVal}`)
lastPage.value = oldVal
})
// 用watch监听单个引用类型props的变化:默认只监听引用地址,要深度监听的话加deep: true
watch(() => props.productList, (newList) => {
console.log('商品列表更新了', newList)
}, { deep: true, immediate: true })
// 这里的immediate: true表示组件刚挂载就执行一次watch回调,适合需要初始化数据的场景
// 用watchEffect自动追踪:只要productList或currentPage变了,就重新计算总价(假设productList有price属性)
watchEffect(() => {
const total = props.productList.reduce((sum, item) => sum + item.price * item.count, 0)
console.log('商品总价更新为', total)
})
</script>
这里要注意两个踩坑点:
- 直接传props.productList而不是用函数包裹:如果props是基础类型或者引用类型但只想监听地址,直接传没问题,但如果props是组合式API里通过defineProps返回的响应式对象的属性,推荐用箭头函数包裹成getter形式,这样能确保追踪的是最新的属性值,避免某些边界情况失效。
- 深度监听引用类型的性能问题:如果productList是个超大数组(比如几万条商品),加deep: true会让Vue3遍历数组里的每一个元素、每一个嵌套对象,性能开销很大,这时候可以考虑用方法二或者方法三,或者只监听引用类型里某个具体的、变化频率高的属性。
组合式API用computed派生状态
如果监听props的目的不是触发复杂函数,而是生成一个新的响应式状态,那用computed比watch/watchEffect更合适——因为computed有缓存机制,只有依赖的props变化时才会重新计算,不会每次渲染都执行。 还是用商品列表的例子,比如父组件传过来productList和筛选条件type,子组件需要展示type对应的商品:
<script setup>
import { computed, defineProps } from 'vue'
const props = defineProps(['productList', 'type'])
// 派生一个筛选后的响应式列表
const filteredList = computed(() => {
if (!props.type) return props.productList
return props.productList.filter(item => item.type === props.type)
})
</script>
<template>
<ul>
<li v-for="product in filteredList" :key="product.id">{{ product.name }}</li>
</ul>
</template>
这里computed的优势就很明显了:不需要手动写watch的回调,只要type或productList的引用地址(或者内部元素,如果用了深层属性但不需要deep?不对,这里filter遍历的是引用地址,地址变了或者内部元素的属性如果影响filter结果的话,computed会不会自动更新?哦对了,Vue3的Proxy是深度响应式的,但前提是父组件传过来的productList本身是响应式的(比如父组件用ref或reactive定义的)——如果父组件是直接传的静态对象数组,那不管内部元素怎么改,引用地址没变,computed和watch都不会更新,这点一定要注意!
组合式API用watchEffect结合toRef/toRefs监听单个/多个props属性
刚才方法一提过watchEffect自动追踪,但如果我们只想追踪props里的几个具体属性,又不想写箭头函数包裹的watch,或者怕直接解构props导致失去响应式(比如const { productList, type } = defineProps(['productList', 'type']),这时候直接用productList和type的话,如果是基础类型,父组件改了,子组件的解构变量不会更新;如果是引用类型,解构变量还是指向同一个地址,内部元素变了会更新,但地址变了不会),那可以用toRef/toRefs。 toRef是把响应式对象的单个属性转成ref,toRefs是把所有属性转成ref——解构props之前用toRefs,就能保证每个解构变量都是响应式的。 举个例子:
<script setup>
import { watchEffect, toRefs, defineProps } from 'vue'
const props = defineProps(['productList', 'type'])
// 用toRefs解构props
const { productList, type } = toRefs(props)
// 这里productList和type都是ref,父组件改的话,不管是基础类型还是引用类型,都会被watchEffect自动追踪
watchEffect(() => {
console.log('productList的长度是', productList.value.length)
console.log('当前筛选类型是', type.value)
})
</script>
这里要注意:如果props里的某个属性一开始没有传(比如type是可选的),直接用toRefs的话,type.value会是undefined,不会有响应式的默认值——如果要设置默认值,最好在defineProps里就设置好,比如defineProps({ type: String, default: '' })。
选项式API用watch选项或computed选项
虽然现在Vue3主推组合式API,但很多老项目还在用选项式API,或者有些开发者更习惯选项式的写法,那Vue3的选项式API也完全支持监听props。 选项式API的watch和computed选项的用法和Vue2差不多,但因为底层换成了Proxy,性能更好,而且支持更清晰的写法。 举个选项式API的例子:
<script>
export default {
props: {
productList: {
type: Array,
default: () => []
},
type: {
type: String,
default: ''
}
},
data() {
return {
lastPage: 1
}
},
computed: {
// 派生筛选后的列表,和组合式API的computed用法一样
filteredList() {
if (!this.type) return this.productList
return this.productList.filter(item => item.type === this.type)
}
},
watch: {
// 监听单个基础类型props
type(newVal, oldVal) {
console.log(`筛选类型从${oldVal}变成了${newVal}`)
this.lastPage = 1
},
// 监听单个引用类型props,加deep: true深度监听,immediate: true立即执行
productList: {
handler(newList) {
console.log('商品列表更新了', newList)
},
deep: true,
immediate: true
}
}
}
</script>
利用Vue3.4+的defineModel简化双向绑定+监听逻辑
哦对了,最近Vue3.4版本更新了一个很实用的API叫defineModel,它可以简化v-model绑定组件props的双向逻辑——以前双向绑定v-model的时候,父组件传modelValue,子组件要emit('update:modelValue'),还要监听modelValue的变化,现在用defineModel就可以把这两步合二为一,而且defineModel返回的是一个ref,本身就是响应式的,修改它会自动触发update:modelValue事件,父组件也会自动更新,不需要再额外监听。 不过defineModel虽然主要是用来做双向绑定的,但如果我们需要在双向绑定的同时做一些额外的操作(比如弹窗打开的时候请求数据,关闭的时候清空数据),那可以直接watch这个defineModel返回的ref,或者在ref的getter/setter里写逻辑(defineModel支持自定义getter/setter)。 举个Vue3.4+的例子,比如一个可搜索的弹窗组件:
<script setup>
// 引入defineModel和watch
import { defineModel, watch } from 'vue'
// 定义v-model绑定的ref,默认值是false(弹窗关闭)
const isVisible = defineModel('visible', { default: false })
// 定义搜索关键词的ref,也是通过v-model绑定,父组件可以传searchKeyword,子组件修改会触发update:searchKeyword
const searchKeyword = defineModel('searchKeyword', { default: '' })
// 自定义getter/setter的defineModel,比如父组件传count,子组件修改count的时候加1
// const count = defineModel('count', {
// default: 0,
// get(val) {
// return val * 2 // 父组件传过来的val先乘2再给子组件用
// },
// set(val) {
// return val + 1 // 子组件改的val先加1再传给父组件
// }
// })
// 监听isVisible的变化,打开弹窗的时候请求数据,关闭的时候清空
watch(isVisible, (newVal) => {
if (newVal) {
console.log('弹窗打开了,请求数据...')
// 这里写请求数据的代码
} else {
console.log('弹窗关闭了,清空数据...')
searchKeyword.value = ''
// 这里写清空数据的代码
}
})
</script>
<template>
<div v-if="isVisible" class="modal">
<input v-model="searchKeyword" placeholder="请输入搜索关键词" />
<button @click="isVisible = false">关闭</button>
</div>
</template>
这里父组件的用法是:
<script setup>
import { ref } from 'vue'
import SearchModal from './SearchModal.vue'
const isModalVisible = ref(false)
const modalSearchKeyword = ref('')
</script>
<template>
<button @click="isModalVisible = true">打开搜索弹窗</button>
<SearchModal v-model:visible="isModalVisible" v-model:searchKeyword="modalSearchKeyword" />
</template>
这个API真的很香,强烈建议大家用Vue3.4+的版本体验一下。
不同场景该选哪种方法?
最后给大家整理一下选择思路,避免乱用:
- 如果只是需要生成新的响应式状态,不需要触发复杂函数:选computed(组合式或选项式都可以),性能最好。
- 如果需要触发复杂函数,比如请求数据、重置状态、修改本地存储:选watch(组合式或选项式都可以),如果是Vue3.4+的双向绑定,也可以watch defineModel返回的ref。
- 如果依赖多个响应式数据联动,不需要手动指定依赖:选watchEffect(组合式API专用)。
- 如果怕解构props导致失去响应式:用toRefs解构后再用watch/watchEffect(组合式API专用)。
- 如果需要做双向绑定+额外的监听操作:选Vue3.4+的defineModel(组合式API专用)。
好了,Vue3监听props变化的所有实用方法和踩坑点就讲完了,希望对大家的项目开发有帮助,如果还有什么问题,欢迎在评论区留言讨论。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

