Vue3中组件间通信详解
Vue3中组件间通信详解
- 一、 父子组件通信(最基础、最高频)
- 1. Props:父传子
- 2. Emits:子传父
- 3. v-model / defineModel:双向绑定
- 4. Ref + DefineExpose:父调用子方法
- 二、 跨层级/祖孙组件通信
- 1. Provide / Inject
- 三、 兄弟组件/任意组件通信
- 1. Mitt 事件总线
- 四、 全局状态管理
- 1.Pinia (推荐)
- 五、 其他特殊场景
- 1. Slots(插槽):内容分发与定制
- 场景 A:默认插槽与具名插槽
- 场景 B:作用域插槽(Scoped Slots)
- 2. $attrs:属性透传(Attribute Inheritance)
- 场景 A:禁用默认继承并手动控制绑定位置
- 场景 B:多层组件透传(高阶组件)
- 六、总结与选型建议
在 Vue3 开发中,组件通信是构建复杂应用的核心技能。根据组件之间的关系(父子、兄弟、跨层级、全局),Vue3 提供了多种高效且规范的通信方案。
一、 父子组件通信(最基础、最高频)
父子组件通信遵循单向数据流原则:父组件通过属性向下传递数据,子组件通过事件向上传递消息。
1. Props:父传子
这是最基础的通信方式,数据由父组件流向子组件,子组件只能读取,不能直接修改。
- 父组件:通过属性绑定传递数据。
- 子组件:使用
defineProps接收数据。支持类型校验、默认值和必填项设置。
<!--父组件 Father.vue--><template><Child:msg="message":count="10"/></template><script setup>import{ref}from'vue'importChildfrom'./Child.vue'constmessage=ref('Hello Vue3')</script><!--子组件 Child.vue--><script setup>// 接收 props,支持类型校验constprops=defineProps({msg:{type:String,required:true},count:{type:Number,default:0}})// 使用 props.msg</script>2. Emits:子传父
当子组件需要通知父组件更新数据或执行操作时,使用自定义事件。
- 子组件:使用
defineEmits定义事件,并通过emit触发。 - 父组件:监听子组件触发的事件并处理。
<!--子组件 Child.vue--><template><button @click="handleClick">发送数据给父组件</button></template><script setup>constemit=defineEmits(['update-data'])consthandleClick=()=>{emit('update-data','来自子组件的数据')}</script><!--父组件 Father.vue--><template><Child @update-data="receiveData"/></template><script setup>constreceiveData=(payload)=>{console.log('收到:',payload)}</script>3. v-model / defineModel:双向绑定
用于实现父子组件数据的双向同步。Vue 3.4+ 推荐使用defineModel宏简化写法。
- 原理:本质上是
props+emit('update:xxx')的语法糖。 - 用法:父组件使用
v-model,子组件使用defineModel。
<!--父组件--><template><Child v-model:title="pageTitle"/></template><!--子组件 Child.vue(Vue3.4+)--><script setup>consttitle=defineModel('title')// 直接修改 title.value 即可同步到父组件</script>4. Ref + DefineExpose:父调用子方法
当父组件需要直接调用子组件内部的方法或访问其状态时使用。注意:这会破坏组件封装性,应谨慎使用。
- 子组件:使用
defineExpose暴露需要被访问的方法或属性。 - 父组件:通过
ref获取子组件实例。
<!--子组件 Child.vue--><script setup>constsayHi=()=>console.log('Hi!')// 暴露方法defineExpose({sayHi})</script><!--父组件 Father.vue--><template><Child ref="childRef"/><button @click="callChild">调用子组件方法</button></template><script setup>import{ref}from'vue'constchildRef=ref(null)constcallChild=()=>{childRef.value?.sayHi()}</script>二、 跨层级/祖孙组件通信
当组件嵌套过深(如 A -> B -> C -> D),使用 Props 逐层传递(Prop Drilling)非常繁琐,此时可采用以下方案:
1. Provide / Inject
祖先组件提供数据,后代组件无论嵌套多深都可以注入使用。适合主题配置、用户信息等深层共享数据。
- 祖先组件:使用
provide提供数据。 - 后代组件:使用
inject注入数据。
<!--祖先组件--><script setup>import{provide,ref}from'vue'consttheme=ref('dark')provide('theme-key',theme)</script><!--深层后代组件--><script setup>import{inject}from'vue'consttheme=inject('theme-key')</script>三、 兄弟组件/任意组件通信
1. Mitt 事件总线
Vue3 移除了原生的$bus,推荐使用轻量级第三方库mitt实现发布-订阅模式。适合非父子、非全局状态的简单通信。
- 注意:需在组件卸载时手动移除监听,防止内存泄漏。
// utils/bus.jsimportmittfrom'mitt'exportconstemitter=mitt()<!--组件A(发送)--><script setup>import{emitter}from'@/utils/bus'constsend=()=>{emitter.emit('my-event','data')}</script><!--组件B(接收)--><script setup>import{onMounted,onUnmounted}from'vue'import{emitter}from'@/utils/bus'consthandler=(data)=>console.log(data)onMounted(()=>{emitter.on('my-event',handler)})onUnmounted(()=>{emitter.off('my-event',handler)// 务必清理})</script>四、 全局状态管理
对于大型项目,涉及多个组件共享复杂状态(如登录态、购物车、权限等),建议使用状态管理库。
1.Pinia (推荐)
Pinia 是 Vue 官方推荐的状态管理库,替代了 Vuex。它去除了 Mutation,仅保留 State、Getter、Action,对 TypeScript 支持更好,且体积更小。
- 特点:模块化设计,无需嵌套模块,调试方便。
- 用法:创建 Store,在组件中直接调用 store 的状态和方法。
// stores/user.jsimport{defineStore}from'pinia'import{ref}from'vue'exportconstuseUserStore=defineStore('user',()=>{constname=ref('Admin')functionsetName(newName){name.value=newName}return{name,setName}})<!--任意组件--><script setup>import{useUserStore}from'@/stores/user'constuserStore=useUserStore()// 直接使用 userStore.name 或 userStore.setName()</script>五、 其他特殊场景
1. Slots(插槽):内容分发与定制
插槽允许父组件向子组件传递模板结构,实现“结构由子组件定,内容由父组件定”的解耦。
场景 A:默认插槽与具名插槽
适用于布局组件(如卡片、弹窗),其中部分区域固定,部分区域需自定义。
子组件Card.vue
<template><divclass="card"><!--具名插槽:头部--><headerclass="card-header"><slot name="header">默认标题</slot></header><!--默认插槽:主体内容--><mainclass="card-body"><slot>默认主体内容</slot></main><!--具名插槽:底部操作区--><footerclass="card-footer"><slot name="footer"><button>默认按钮</button></slot></footer></div></template><style scoped>.card{border:1px solid ddd;padding:10px;}.card-header{font-weight:bold;margin-bottom:10px;}.card-footer{margin-top:10px;text-align:right;}</style>父组件Parent.vue使用
<template><Card><!--1.具名插槽写法:v-slot:name 或简写 name--><template header><h2>自定义标题</h2></template><!--2.默认插槽:直接写内容,无需 template 包裹(也可用default)--><p>这是父组件传入的动态正文内容。</p><!--3.具名插槽:底部--><template footer><button @click="save">保存</button><button @click="cancel">取消</button></template></Card></template><script setup>importCardfrom'./Card.vue'constsave=()=>console.log('保存')constcancel=()=>console.log('取消')</script>场景 B:作用域插槽(Scoped Slots)
适用于列表、表格等组件,子组件提供数据,父组件决定如何渲染这些数据。
子组件UserList.vue
<template><ul><li v-for="user in users":key="user.id"><!--通过:user="user"将数据暴露给父组件 父组件接收时可以使用任意变量名,如 slotProps--><slot:user="user":index="user.id"><!--默认渲染方式-->{{user.name}}</slot></li></ul></template><script setup>import{ref}from'vue'constusers=ref([{id:1,name:'Alice',role:'Admin'},{id:2,name:'Bob',role:'User'}])</script>父组件Parent.vue使用
<template><UserList><!--v-slot:default="slotProps"slotProps 包含子组件暴露的所有属性:{user,index}--><templatedefault="{ user, index }"><divclass="custom-item"><span>{{index}}.</span><strong>{{user.name}}</strong><span v-if="user.role === 'Admin'"class="badge">管理员</span></div></template></UserList></template><script setup>importUserListfrom'./UserList.vue'</script><style scoped>.badge{background:red;color:white;padding:2px 5px;font-size:12px;}</style>2. $attrs:属性透传(Attribute Inheritance)
$attrs包含父组件传递给子组件但未被声明为 props 且未被监听为 emits 的所有属性(包括 class, style, 事件监听器等)。常用于高阶组件或封装第三方库组件。
场景 A:禁用默认继承并手动控制绑定位置
默认情况下,未声明的属性会自动绑定到组件的根元素。如果希望绑定到内部特定元素(如 input),需禁用默认继承。
子组件CustomInput.vue
<template><divclass="input-wrapper"><label>标签:</label><!--1.defineOptions({inheritAttrs:false})禁用了自动绑定到根 div2.v-bind="$attrs"手动将属性绑定到 input 上 这样父组件传的 placeholder,@focus,class等都会生效在 input 上--><input v-bind="$attrs"/></div></template><script setup>// Vue 3.3+ 推荐写法defineOptions({inheritAttrs:false})// 注意:这里没有定义任何 props,所以所有传入属性都会进入 $attrs</script>父组件Parent.vue使用
<template><!--placeholder,@focus,class均未在子组件 props 中声明 它们会通过 $attrs 透传到内部的 input 元素上--><CustomInput placeholder="请输入内容"@focus="onFocus"class="custom-style"/></template><script setup>importCustomInputfrom'./CustomInput.vue'constonFocus=()=>console.log('输入框获得焦点')</script>场景 B:多层组件透传(高阶组件)
当组件只是作为中间层,需要将属性继续传递给更深层的子组件时。
中间组件Wrapper.vue
<template><divclass="wrapper-border"><!--将接收到的所有未声明属性透传给 BaseButton--><BaseButton v-bind="$attrs"><!--如果需要透传插槽,也可以显式写出--><slot></slot></BaseButton></div></template><script setup>importBaseButtonfrom'./BaseButton.vue'// Wrapper 不声明任何 props,所有属性直接透传</script>底层组件BaseButton.vue
<template><button v-bind="$attrs"><slot></slot></button></template><script setup>// 同样不声明 props,确保属性最终绑定到 button 元素defineOptions({inheritAttrs:false})</script>父组件使用
<template><!--type,disabled,@click 等属性会穿过 Wrapper,最终绑定到 BaseButton 的 button 标签上--><Wrapper type="submit"disabled @click="handleSubmit">提交</Wrapper></template>六、总结与选型建议
| 场景 | 推荐方案 | 备注 |
|---|---|---|
| 父传子 | props | 单向数据流,最基础 |
| 子传父 | emits | 保持单向流,解耦 |
| 父子双向同步 | v-model/defineModel | 表单、输入框常用 |
| 父调用子方法 | ref+defineExpose | 慎用,破坏封装 |
| 跨多层级传递 | provide/inject | 避免 Prop Drilling |
| 兄弟/任意组件 | Mitt事件总线 | 轻量级,需手动清理监听 |
| 大型全局状态 | Pinia | 官方推荐,替代 Vuex |
| 内容布局定制 | Slots | 特别是作用域插槽 |
在实际开发中,应优先使用 Props/Emits 和 Pinia,仅在特定场景下使用 Provide/Inject 或 Mitt,以保持代码的可维护性和清晰的数据流向。