在 Vue3 中,组件通信方式根据组件关系(父子、祖孙、兄弟、跨级等)的不同,有多种实现方式,所以选择合适的通信方式可以使我们的开发事半功倍。
一、Props / Emits
1.1 Props (父向子通信)
适用场景: 父子组件之前传值、兄弟组件通过父组件传值通信
原理:父组件通过属性绑定传递数据,子组件通过 props 接收并使用。
实现步骤:
-
父组件在引用子组件时,通过
v-bind(简写:)传递数据:<template> <div class="comp-parent"> <!-- title属于静态属性 通过v-bind绑定响应式数据count属性 --> <CompChild title="子组件标题" :count="count" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import CompChild from './comp-child.vue' const count = ref(0) </script> -
子组件通过
defineProps定义接收的props(支持类型校验、默认值等):
<template>
<div class="comp-child">
<h1>{{ title }}</h1>
<p>当前计数:{{ count }}</p>
</div>
</template>
<script setup lang="ts">
// 方式一:通过defineProps定义子组件接收的属性
// defineProps({
// title: {
// type: String,
// default: '',
// },
// count: {
// type: Number,
// default: 0,
// },
// })
// 方式二: 使用 TypeScript 类型注解
defineProps<{
title: string
count?: number
}>()
</script>
1.2 Emits(自定事件:子向父传值)
适用场景: 子组件向上传值
原理: 子组件通过触发自定义事件传递数据,父组件监听事件并接收数据。
实现步骤:
-
子组件通过触发自定义事件传递数据,父组件监听事件并接收数据。
<template> <div class="comp-parent"> <!-- title属于静态属性 通过v-bind绑定响应式数据count属性, 子组件通过addCount事件触发父组件更新count --> <CompChild title="子组件标题" :count="count" @addCount="addCount" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import CompChild from './comp-child.vue' const count = ref(0) // 定义addCount方法 const addCount = (val: number) => { console.log('接受子组件count加一事件: ', val) count.value = val } </script> -
父组件监听子组件的自定义事件,通过回调接收数据:
<template>
<div class="comp-child">
<h1>{{ title }}</h1>
<p>当前计数:{{ count }}</p>
<button @click="handleClick">count加一</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string
count?: number
}>()
// 使用 defineEmits
const emit = defineEmits(['addCount'])
const handleClick = () => {
if (typeof props?.count !== 'undefined') {
emit('addCount', props.count + 1)
}
}
</script>
二、 V-model(父子双向绑定)
适用场景: 父子组件传值通信
原理: 基于 props 和 emits 的语法糖,简化父子组件数据双向同步。
实现步骤:
-
父组件使用
v-model绑定数据:<template> <div class="comp-parent"> <CompChild v-model:count="count" /> <!-- 等价于:<CompChild :modelValue="count" @update:modelValue="count = $event" /> --> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import CompChild from './comp-child.vue' const count = ref(0) </script> -
子组件通过
modelValue接收数据,通过update:modelValue事件更新:<template> <div class="comp-child"> <p>当前计数:{{ count }}</p> <button @click="handleClick">count加一</button> </div> </template> <script setup lang="ts"> const props = defineProps<{ count: number }>() // 使用 defineEmits const emit = defineEmits(['update:count']) const handleClick = () => { if (typeof props?.count !== 'undefined') { emit('update:count', props.count + 1) } } </script>
三、 ref / 模板引用(父组件访问子组件实例)
适用场景: 父子组件传值
原理: 父组件通过 ref 获取子组件实例,直接访问子组件的属性或方法(需子组件主动暴露)。
实现步骤:
-
子组件通过
defineExpose暴露需要被父组件访问的内容:<template> <div class="comp-child"> <h1>{{ title }}</h1> </div> </template> <script setup lang="ts"> import { ref } from 'vue' const title = ref<string>('子组件标题') // 暴露给父组件属性和方法 defineExpose({ title, changeTitle(newTitle: string) { title.value = newTitle }, }) </script> -
父组件通过
ref绑定子组件,获取实例并访问:
<template>
<div class="comp-parent">
<CompChild ref="childRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import CompChild from './comp-child.vue'
// 子组件实例引用
const childRef = ref<typeof CompChild>()
// 实例挂载完成后调用
onMounted(() => {
console.log('子组件标题:', childRef.value?.title)
childRef.value?.changeTitle('父组件更改的子组件标题')
})
</script>
四、Provide / Inject (依赖注入)
适用场景: 父子组件传值、祖先组件传值
原理: 祖先组件通过 provide 提供数据,任意后代组件通过 inject 注入并使用,无视层级。
使用步骤:
-
祖先组件通过provide提供数据:
<template> <div class="comp-parent"> <CompChild /> </div> </template> <script setup lang="ts"> import { ref, provide } from 'vue' import CompChild from './comp-child.vue' // 用户信息 const userInfo = ref({ name: '懒羊羊我小弟', age: 18, }) // 传给子孙的属性和方法 provide('userInfo', userInfo) </script> -
后代组件(子、孙等)通过inject 注入数据:
<template> <div class="comp-child"> <h1>{{ userInfo?.name }}</h1> </div> </template> <script setup lang="ts"> // 接收祖先传递的参数 import { inject } from 'vue' // 接收祖先传递的用户信息 const userInfo = inject<{ name: string; age: number }>('userInfo') </script>
五、 Event Bus (全局事件总线)
适用场景: 兄弟组件,没有直接关系的组件
原理:通过一个全局的事件中心,组件可触发事件或监听事件,实现解耦通信。
实现步骤:
-
定义一个事件总线实例
// eventBus.js import { ref } from 'vue' class EventBus { constructor() { this.events = ref({}) } $on(event, callback) { if (!this.events.value[event]) { this.events.value[event] = [] } this.events.value[event].push(callback) } $off(event, callback) { if (!this.events.value[event]) return if (callback) { this.events.value[event] = this.events.value[event].filter((cb) => cb !== callback) } else { delete this.events.value[event] } } $emit(event, ...args) { if (this.events.value[event]) { this.events.value[event].forEach((callback) => { callback(...args) }) } } } export const eventBus = new EventBus() // 或者使用 mitt 库 // import mitt from 'mitt' // export const eventBus = mitt() -
在需要使用的组件中引入实例并监听自定函数
<!--> 父组件<--> <template> <div class="comp-parent"> <CompChild /> </div> </template> <script setup lang="ts"> import { onMounted } from 'vue' import CompChild from './comp-child.vue' import { eventBus } from '@/utils/event-bus' // 触发事件 onMounted(() => { eventBus.$emit('userInfoChange', { name: '懒羊羊我小弟', age: 18 }) }) </script> <!--> 子组件组件<--> <template> <div class="comp-child"> <h1>{{ userInfo?.name }}</h1> </div> </template> <script setup lang="ts"> // 接收祖先传递的参数 import { ref, onMounted, onUnmounted } from 'vue' import { eventBus } from '@/utils/event-bus' const userInfo = ref<{ name: string; age: number }>({ name: '', age: 0 }) // 监听事件 onMounted(() => { eventBus.$on('userInfoChange', (newUserInfo: { name: string; age: number }) => { userInfo.value = newUserInfo }) }) // 取消监听事件 onUnmounted(() => { eventBus.$off('userInfoChange') }) </script>
六、 Attrs(属性透传)
适用场景: 父子组件通信、组件库组件的属性继承
原理: 当组件嵌套层级较深时(如 A → B → C),若 A 要给 C 传递属性,无需 B 手动通过 props 接收再传递给 C,而是可以通过 B 中的 $attrs 直接 “透传” 给 C,减少中间层的代码冗余。
实现步骤:
-
传递属性给子组件,但子组件不声明
props<template> <div class="comp-parent"> <CompChild id="comp-child" :userInfo="userInfo" @changeTitle="changeTitle" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import CompChild from './comp-child.vue' const userInfo = ref<{ name: string; age: number }>({ name: '懒羊羊我小弟', age: 18 }) // 定义一个方法,用于改变子组件的title const changeTitle = (title: string) => { userInfo.value.name = title } </script> -
子组件通过useAttrs获取到未被defineProps获取的属性值
<template> <div class="comp-child"> <h1>{{ userInfo?.name }}</h1> </div> </template> <script setup lang="ts"> import { useAttrs, onMounted } from 'vue' // 获取未被defineProps定义的属性 const $attrs = useAttrs() console.log('attrs: ', $attrs) // 接收需要的props传参 defineProps({ userInfo: { type: Object, default: () => {}, }, }) onMounted(() => { if ($attrs.onChangeTitle) { // 调用子组件传递的方法 ;($attrs.onChangeTitle as (title: string) => void)('懒羊羊我大哥') } }) </script>补充:
-
默认情况下,
$attrs中的属性会自动添加到组件的根元素上(除了已被props声明的)。若想禁用这一行为,可在组件中设置inheritAttrs: false。<!-- 子组件 Child.vue --> <template> <div>子组件</div> <Grandchild v-bind="$attrs" /> <!-- 手动透传给孙组件 --> </template> <script setup> // Vue3 中在 setup 外声明 inheritAttrs export default { inheritAttrs: false // 根元素不再自动添加 $attrs 中的属性 } </script> -
$attrs中的属性是 “剩余” 未被声明的属性,遵循 “声明优先” 原则。 -
可以选择性透传:
v-bind="{ ...$attrs, otherProp: 'value' }"(扩展运算符)。 -
避免过度透传:
$attrs适合简单的透传场景,若透传层级过深或属性 / 事件过多,建议使用provide/inject或状态管理库,避免维护困难。
七、Slots(插槽通信)
-
适用场景: 子组件向父组件通信
原理: 子组件在定义插槽时,通过 v-bind(或简写 :)将内部数据绑定到插槽上,这些数据会成为 “插槽作用域” 的一部分,供父组件使用。
实现步骤:
-
在子组件中预留插槽位置,将需要传递出去的数据通过
v-bind绑定<template> <div class="comp-child"> <!-- 定义header插槽,接收title属性 --> <slot name="header" :title="title"></slot> </div> </template> <script setup lang="ts"> import { ref } from 'vue' const title = ref<string>('懒羊羊我小弟') </script> -
父组件通过插槽来获取子组件数据展示到页面上
<template> <div class="comp-parent"> <CompChild> <template #header="{ title }"> <h1>{{ title }}</h1> </template> </CompChild> </div> </template> <script setup lang="ts"> import { useSlots } from 'vue' import CompChild from './comp-child.vue' const slots = useSlots() console.log('slots: ', slots.header) </script>
八、Pina、VueX(状态管理库)
适用场景: 适用各种场景的组件通信
原理: 通过第三方状态管理库,去开辟不同的独立存储空间,可以处理各种场景的数据通信;
实现步骤(以Pina为例):
-
定义pina储存仓库
import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = defineStore('user', () => { // Composition API 风格 const user = ref<{ name: string; age: number } | null>(null) function login(userData: { name: string; age: number }) { console.log('userData: ', userData) user.value = userData } return { user, login, } }) -
在仓库中获取要展示的数据
<!--> 子组件 <--> <template> <div class="comp-child"> <h1>{{ user?.name }}</h1> </div> </template> <script setup lang="ts"> import { storeToRefs } from 'pinia' // 引入 用户信息userStore import { useUserStore } from '@/stores/user' // 从userStore中获取user状态 const userStore = useUserStore() // 通过storeToRefs将user状态转换为响应式引用 const { user } = storeToRefs(userStore) </script> -
调用方法更改仓库数据
<!--> 父组件 <--> <template> <div class="comp-parent"> <CompChild /> </div> </template> <script setup lang="ts"> import { useUserStore } from '@/stores/user' import CompChild from './comp-child.vue' // 引入 用户信息userStore const userStore = useUserStore() // 调用userStore的login方法,登录用户 userStore.login({ name: '懒羊羊我小弟', age: 18 }) </script>
九、 总结
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props/Emits | 父子组件通信 | 简单直接,类型安全 | 不适合深层组件 |
| v-model | 表单组件双向绑定 | 语法简洁,符合习惯 | 仅限于表单场景 |
| ref | 调用子组件方法 | 直接访问子组件 | 破坏组件封装性 |
| Provide/Inject | 深层组件通信 | 避免逐层传递 | 数据流向不明确 |
| Event Bus | 任意组件通信 | 完全解耦,灵活 | 难以调试和维护 |
| Pinia | 复杂状态管理 | 功能强大,可调试 | 需要学习成本 |
| Attrs | 属性透传 | 灵活处理未知属性 | 可能造成属性冲突 |
| Slots | 内容分发 | 高度灵活,可定制 | 语法相对复杂 |
选择建议:
- 简单父子通信:Props/Emits
- 表单组件:v-model
- 深层组件:Provide/Inject
- 复杂应用状态:Pinia
- 临时全局事件:Event Bus
- UI 组件库:Slots + Attrs
根据具体场景选择合适的通信方式,保持代码的可维护性和可读性。
1万+

被折叠的 条评论
为什么被折叠?



