图秀主页
56.67M · 2026-02-04
在 Vue.js 的组件化开发中,组件间的通信是构建复杂应用的核心。Vue3 相比 Vue2 在组件通信方面有了显著的变化和改进,这些改进不仅简化了开发流程,还提供了更强大、更灵活的数据传递方式。本文将深入探讨 Vue3 中各种组件通信方式,并结合实际代码示例进行详细说明。
在开始具体通信方式之前,让我们先了解 Vue3 相对于 Vue2 在组件通信方面的主要变化:
这些变化使 Vue3 的组件通信更加简洁、一致,同时保持了强大的功能。
Props 是 Vue 中最基础的组件通信方式,用于父组件向子组件传递数据。
关键点:
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 接收 propssendToy(toy) 将数据传回父组件| 特性 | 原生事件 | 自定义事件 |
|---|---|---|
| 事件名 | 特定的(click、mouseenter等) | 任意名称 |
| 事件对象 | 包含事件相关信息的对象 | 调用 emit 时传递的数据 |
| 命名规范 | - | 建议使用烤肉串写法(如 send-toy) |
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>
Mitt 是一个小巧的发布-订阅库,用于在 Vue3 中实现任意组件间的通信。
核心 API:
on:事件off:移除事件emit:触发事件all.clear:清除所有事件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 时,一定要记得在组件卸载时移除事件,避免内存泄漏。
在 Vue3 中,组件上的 v-model 本质上是 :modelValue 和 @update:modelValue 的语法糖。
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>
Vue3 支持在单个组件上使用多个 v-model:
vue
复制下载
<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
实现原理:
v-model:abc 对应 :abc 和 @update:abcv-model:xyz 对应 :xyz 和 @update:xyz$attrs 用于实现父组件向孙组件通信,它包含了父组件传入的所有非 prop 属性。
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 中声明的属性,这些属性被子组件自己"消费"了。
$refs:用于父组件访问子组件实例(父→子)$parent:用于子组件访问父组件实例(子→父)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 宏函数将数据暴露给外部组件使用。
Provide 和 Inject 用于实现祖孙组件间的直接通信,无需经过中间组件。
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 在后代组件中注入数据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>
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>
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>
作用域插槽的核心思想:数据在子组件中,但数据的渲染结构由父组件决定。
虽然本文主要代码示例中没有包含 Pinia 的具体实现,但作为 Vue3 推荐的状态管理库,它在复杂应用中的组件通信中扮演着重要角色。
Pinia 的优势:
通过以上九种通信方式的详细分析,我们可以总结出 Vue3 组件通信的最佳实践:
| 通信场景 | 推荐方式 | 说明 |
|---|---|---|
| 父子组件通信 | Props / 自定义事件 | 简单直接,符合单向数据流 |
| 子父组件通信 | 自定义事件 | 通过事件传递数据 |
| 双向绑定 | v-model | 简洁高效 |
| 任意组件通信 | Mitt / Pinia | 根据复杂度选择 |
| 祖孙组件通信 | Provide/Inject / $attrs | 避免 prop 逐层传递 |
| 组件实例访问 | parent | 谨慎使用,破坏封装性 |
| 内容分发 | 插槽 | 灵活的内容分发机制 |
Vue3 的组件通信机制在继承 Vue2 优秀设计的基础上,进行了许多优化和改进。从简单的 props 到复杂的状态管理,Vue3 提供了丰富的工具来满足不同场景下的通信需求。理解并合理运用这些通信方式,将帮助开发者构建更加健壮、可维护的 Vue3 应用。
在实际开发中,建议根据具体场景选择合适的通信方式,遵循 Vue 的设计哲学,保持组件的独立性和可复用性。