vue3的组件通信方式汇总
在 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: stringcount?: 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: stringcount?: 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]) returnif (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 外声明 inheritAttrsexport 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
根据具体场景选择合适的通信方式,保持代码的可维护性和可读性。