引言
在 Vue.js 的组件化开发中,组件间的通信是构建复杂应用的核心。Vue3 相比 Vue2 在组件通信方面有了显著的变化和改进,这些改进不仅简化了开发流程,还提供了更强大、更灵活的数据传递方式。本文将深入探讨 Vue3 中各种组件通信方式,并结合实际代码示例进行详细说明。
Vue3 与 Vue2 组件通信的主要区别
在开始具体通信方式之前,让我们先了解 Vue3 相对于 Vue2 在组件通信方面的主要变化:
- 移除事件总线,使用 mitt 代替:Vue3 移除了内置的事件总线系统,推荐使用第三方库 mitt 来实现任意组件间的通信
- Vuex 换成了 Pinia:状态管理库从 Vuex 迁移到更轻量、类型安全的 Pinia
- .sync 修饰符优化到 v-model:Vue3 将 .sync 修饰符的功能整合到了 v-model 中,使双向绑定更加统一
- attrs:简化了属性传递机制
- 移除了 $children:简化组件实例访问方式
这些变化使 Vue3 的组件通信更加简洁、一致,同时保持了强大的功能。
1. Props:父子组件的基础通信方式
1.1 基本概念
Props 是 Vue 中最基础的组件通信方式,用于父组件向子组件传递数据。
关键点:
- 父传子:属性是非函数
- 子传父:属性是函数(通过回调函数实现)
1.2 示例分析
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<h4>汽车:{{ car }}</h4>
<h4 v-show="toy">父接收到子的玩具:{{ toy }}</h4>
<!-- 传递数据和回调函数给子组件 -->
<Child :car="car" :sendToy="getTony"/>
</div>
</template>
<script setup lang="ts" name="Father">
import {ref} from 'vue';
import Child from './Child.vue';
let car = ref('奔驰');
let toy = ref('');
function getTony(value:string){
console.log('父接收到子的玩具',value);
toy.value = value;
}
</script>
vue
复制下载
<!-- 子组件 Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<h4>父给子的汽车:{{ car }}</h4>
<!-- 通过调用父组件传递的函数实现子传父 -->
<button @click="sendToy(toy)">点击子给父传递玩具</button>
</div>
</template>
<script setup lang="ts" name="Child">
import {ref} from 'vue';
let toy = ref('奥特曼');
// 接收父组件传递的props
defineProps(['car','sendToy']);
</script>
代码解析:
- 父组件通过
:car="car"将数据传递给子组件 - 父组件通过
:sendToy="getTony"将回调函数传递给子组件 - 子组件通过
defineProps接收 props - 子组件通过调用
sendToy(toy)将数据传回父组件
2. 自定义事件:子组件向父组件通信
2.1 原生事件 vs 自定义事件
| 特性 | 原生事件 | 自定义事件 |
|---|---|---|
| 事件名 | 特定的(click、mouseenter等) | 任意名称 |
| 事件对象 | 包含事件相关信息的对象 | 调用 emit 时传递的数据 |
| 命名规范 | - | 建议使用烤肉串写法(如 send-toy) |
2.2 示例实现
vue
复制下载
<!-- 子组件 Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<h4>玩具:{{ toy }}</h4>
<button @click="sendToy">传递玩具给父</button>
</div>
</template>
<script setup lang="ts" name="Child">
import {ref} from 'vue';
let toy = ref('小汽车');
// 定义自定义事件
const emit = defineEmits(['sendToy']);
function sendToy(){
// 触发自定义事件,并传递参数
emit('sendToy', toy.value);
}
</script>
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<h4 v-show="toy">收到的玩具:{{ toy }}</h4>
<!-- 监听子组件的自定义事件 -->
<Child @sendToy="saveToy"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue';
import { ref } from 'vue';
let toy = ref('');
function saveToy(value:string){
toy.value = value;
console.log('父组件接收到子组件传递的玩具:', toy.value);
}
</script>
3. Mitt:任意组件间的事件通信
3.1 Mitt 简介
Mitt 是一个小巧的发布-订阅库,用于在 Vue3 中实现任意组件间的通信。
核心 API:
on:监听事件off:移除事件监听emit:触发事件all.clear:清除所有事件监听
3.2 示例实现
typescript
复制下载
// emitter.ts
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
vue
复制下载
<!-- 发送组件 Child1.vue -->
<template>
<div class="child1">
<h3>子组件1</h3>
<h4>玩具:{{ toy }}</h4>
<button @click="emitter.emit('send-toy',toy)">玩具给Child2</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import {ref} from "vue";
import emitter from '@/utils/emitter';
let toy= ref("奥特曼");
</script>
vue
复制下载
<!-- 接收组件 Child2.vue -->
<template>
<div class="child2">
<h3>子组件2</h3>
<h4>电脑:{{ computer }}</h4>
<h4 v-show="toy">玩具:{{ toy }}</h4>
</div>
</template>
<script setup lang="ts" name="Child2">
import {ref,onUnmounted} from "vue";
import emitter from '@/utils/emitter';
let computer = ref("联想");
let toy = ref("");
// 监听事件
emitter.on('send-toy',(value : string)=>{
console.log('sent-toy');
toy.value = value ;
})
// 组件卸载时移除事件监听
onUnmounted(()=>{
emitter.off('send-toy');
})
</script>
重要提示:使用 mitt 时,一定要记得在组件卸载时移除事件监听,避免内存泄漏。
4. v-model:双向绑定的升级版
4.1 v-model 的本质
在 Vue3 中,组件上的 v-model 本质上是 :modelValue 和 @update:modelValue 的语法糖。
4.2 基本使用示例
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<!-- 使用 v-model 进行双向绑定 -->
<Test v-model="username"/>
</div>
</template>
<script setup lang="ts" name="Father">
import{ref} from "vue";
import Test from './Test.vue';
let username = ref("张三");
</script>
vue
复制下载
<!-- 子组件 Test.vue -->
<template>
<div>
<input type="text"
:value="modelValue"
@input="emit('update:modelValue',(<HTMLInputElement>$event.target).value)"
>
</div>
</template>
<script setup lang="ts" name="Test">
defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>
4.3 多个 v-model 绑定
Vue3 支持在单个组件上使用多个 v-model:
vue
复制下载
<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
实现原理:
v-model:abc对应:abc和@update:abcv-model:xyz对应:xyz和@update:xyz
5. $attrs:祖孙组件通信的桥梁
5.1 $attrs 的作用
$attrs 用于实现父组件向孙组件通信,它包含了父组件传入的所有非 prop 属性。
5.2 示例实现
vue
复制下载
<!-- 祖组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<!-- 传递多个属性和方法 -->
<Child
:a="a"
:b="b"
:c="c"
:d="d"
v-bind="{ x: 100, y: 200 }"
:updateA="updateA"
/>
</div>
</template>
vue
复制下载
<!-- 子组件 Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<!-- 使用 v-bind="$attrs" 将属性传递给孙组件 -->
<GrandChild v-bind="$attrs" />
</div>
</template>
vue
复制下载
<!-- 孙组件 GrandChild.vue -->
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>a:{{ a }}</h4>
<h4>b:{{ b }}</h4>
<h4>c:{{ c }}</h4>
<h4>d:{{ d }}</h4>
<h4>x:{{ x }}</h4>
<h4>y:{{ y }}</h4>
<!-- 调用祖组件传递的方法 -->
<button @click="updateA(6)">点我将爷爷那的a更新</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
// 接收所有从祖组件传递过来的属性
defineProps(['a', 'b', 'c', 'd', 'x', 'y', 'updateA'])
</script>
注意:$attrs 会自动排除在 props 中声明的属性,这些属性被子组件自己"消费"了。
6. parent:组件实例的直接访问
6.1 基本概念
$refs:用于父组件访问子组件实例(父→子)$parent:用于子组件访问父组件实例(子→父)
6.2 示例实现
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<h4>房产:{{ house }}</h4>
<button @click="changeToy">修改Child1的玩具</button>
<button @click="changeComputer">修改Child2的电脑</button>
<button @click="getAllChild($refs)">获取所有的子组件实列对象</button>
<!-- 使用 ref 标识子组件 -->
<Child1 ref="c1"/>
<Child2 ref="c2"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue';
let house = ref(4);
let c1 = ref();
let c2 = ref();
function changeToy(){
// 通过 ref 访问子组件实例并修改其数据
c1.value.toy = '小猪佩奇'
}
function changeComputer(){
c2.value.computer = '戴尔'
}
function getAllChild(refs:any){
console.log(refs);
for(let key in refs){
refs[key].book += 2;
}
}
// 将数据暴露给子组件
defineExpose({house});
</script>
vue
复制下载
<!-- 子组件 Child1.vue -->
<template>
<div class="child1">
<h3>Child1</h3>
<h4>玩具:{{ toy }}</h4>
<h4>书籍:{{ book }}</h4>
<!-- 通过 $parent 访问父组件实例 -->
<button @click="minusHouse($parent)">干掉父亲的一套房产</button>
</div>
</template>
<script setup lang="ts" name="Child1">
import {ref} from 'vue';
let toy = ref('奥特曼');
let book = ref(3);
function minusHouse(parent : any){
console.log(parent);
// 修改父组件的数据
parent.house -=1;
}
// 将数据暴露给父组件
defineExpose({toy, book});
</script>
重要提示:使用 defineExpose 宏函数将数据暴露给外部组件使用。
7. Provide 和 Inject:依赖注入
7.1 基本概念
Provide 和 Inject 用于实现祖孙组件间的直接通信,无需经过中间组件。
7.2 示例实现
vue
复制下载
<!-- 祖组件 Father.vue -->
<template>
<div class="father">
<h3>父组件</h3>
<h4>银子:{{ money }}</h4>
<h4>车子:一辆{{ car.brand }}车, {{ car.price }}万元</h4>
<Child/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref , reactive ,provide} from 'vue'
let money = ref(1000)
function changeMoney(value:number){
money.value -= value;
}
let car = reactive({
brand:'奔驰',
price:100
})
// 向后代提供数据
provide('moneyContext',{money,changeMoney});
provide('che',car);
</script>
vue
复制下载
<!-- 孙组件 GrandChild.vue -->
<template>
<div class="grand-child">
<h3>孙组件</h3>
<h4>银子:{{ money }}</h4>
<h4>车子:一辆{{ car.brand }}车, {{ car.price }}万元</h4>
<!-- 调用祖组件提供的方法 -->
<button @click="changeMoney(100)">花爷爷的银子</button>
</div>
</template>
<script setup lang="ts" name="GrandChild">
import { inject } from 'vue'
// 注入祖组件提供的数据和方法
let {money,changeMoney} = inject('moneyContext',{money:null,changeMoney:(value:number)=>{}});
let car = inject('che',{brand:'',price:0});
</script>
关键点:
- 使用
provide在祖先组件中提供数据 - 使用
inject在后代组件中注入数据 - 可以传递响应式数据和函数
8. 插槽:内容分发的艺术
8.1 默认插槽
vue
复制下载
<!-- 子组件 Category.vue -->
<template>
<div class="category">
<h2>{{title}}</h2>
<!-- 默认插槽 -->
<slot>默认内容</slot>
</div>
</template>
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div>
<h3>父组件</h3>
<div class="content">
<Category title="游戏列表">
<!-- 插入内容到默认插槽 -->
<ul>
<li v-for="g in games" :key="g.id">{{g.name}}</li>
</ul>
</Category>
</div>
</div>
</template>
8.2 具名插槽
vue
复制下载
<!-- 子组件 Category.vue -->
<template>
<div class="category">
<!-- 具名插槽 -->
<slot name="s1">默认内容1</slot>
<slot name="s2">默认内容2</slot>
</div>
</template>
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div>
<h3>父组件</h3>
<div class="content">
<Category>
<!-- 使用 v-slot 指令指定插槽名称 -->
<template v-slot:s1>
<h2>游戏列表</h2>
</template>
<template v-slot:s2>
<ul>
<li v-for="g in games" :key="g.id">{{g.name}}</li>
</ul>
</template>
</Category>
</div>
</div>
</template>
8.3 作用域插槽
vue
复制下载
<!-- 子组件 Game.vue -->
<template>
<div class="game">
<slot name="s1"></slot>
<!-- 作用域插槽,传递数据给父组件 -->
<slot name="s2" :games="games"></slot>
</div>
</template>
vue
复制下载
<!-- 父组件 Father.vue -->
<template>
<div>
<h3>父组件</h3>
<div class="content">
<Game>
<template v-slot:s1>
<h2>游戏列表1</h2>
</template>
<!-- 接收子组件传递的数据 -->
<template v-slot:s2="{ games }">
<ul>
<li v-for="g in games" :key="g.id">{{g.name}}</li>
</ul>
</template>
</Game>
</div>
</div>
</template>
作用域插槽的核心思想:数据在子组件中,但数据的渲染结构由父组件决定。
9. Pinia:现代化的状态管理
虽然本文主要代码示例中没有包含 Pinia 的具体实现,但作为 Vue3 推荐的状态管理库,它在复杂应用中的组件通信中扮演着重要角色。
Pinia 的优势:
- 类型安全:完整的 TypeScript 支持
- 模块化:每个 store 都是独立的模块
- 轻量级:相比 Vuex 更简洁
- 组合式 API:与 Vue3 的组合式 API 完美结合
总结:Vue3 组件通信的最佳实践
通过以上九种通信方式的详细分析,我们可以总结出 Vue3 组件通信的最佳实践:
通信方式选择指南
| 通信场景 | 推荐方式 | 说明 |
|---|---|---|
| 父子组件通信 | Props / 自定义事件 | 简单直接,符合单向数据流 |
| 子父组件通信 | 自定义事件 | 通过事件传递数据 |
| 双向绑定 | v-model | 简洁高效 |
| 任意组件通信 | Mitt / Pinia | 根据复杂度选择 |
| 祖孙组件通信 | Provide/Inject / $attrs | 避免 prop 逐层传递 |
| 组件实例访问 | parent | 谨慎使用,破坏封装性 |
| 内容分发 | 插槽 | 灵活的内容分发机制 |
性能优化建议
- 避免过度使用 parent:这些方式会破坏组件的封装性,增加耦合度
- 合理使用 Provide/Inject:对于深层嵌套的组件通信非常高效
- 及时清理事件监听:使用 mitt 时,记得在组件卸载时移除监听
- 合理分割状态:使用 Pinia 管理全局状态,避免 props 传递过深
代码可维护性建议
- 保持单向数据流:尽量使用 props 向下传递,事件向上传递
- 明确的接口定义:使用 TypeScript 明确 props 和事件类型
- 适度的组件拆分:避免组件过于复杂,合理拆分提高可维护性
- 统一的命名规范:事件名使用烤肉串写法,props 使用驼峰式
结语
Vue3 的组件通信机制在继承 Vue2 优秀设计的基础上,进行了许多优化和改进。从简单的 props 到复杂的状态管理,Vue3 提供了丰富的工具来满足不同场景下的通信需求。理解并合理运用这些通信方式,将帮助开发者构建更加健壮、可维护的 Vue3 应用。
在实际开发中,建议根据具体场景选择合适的通信方式,遵循 Vue 的设计哲学,保持组件的独立性和可复用性。
本文所有示例代码均基于实际项目代码整理,可以直接在 Vue3 + TypeScript 项目中运行测试。通过实践这些示例,开发者可以深入理解 Vue3 组件通信的各种机制,为构建复杂的 Vue3 应用打下坚实基础。