Vue3 组件通信详解:从基础到高级实践
在Vue3中,组件通信是构建复杂应用的核心技能。与Vue2相比,Vue3在组件通信方面有了显著的变化和改进。本文将详细介绍Vue3中的各种组件通信方式,并通过实际示例帮助您深入理解。
Vue3组件通信概述
Vue3与Vue2的主要区别
Vue3在组件通信方面进行了重要优化:
-
移出事件总线:使用mitt库替代
-
状态管理:Vuex换成了Pinia
-
双向绑定优化:将.sync优化到v-model中
-
属性合并:把$listeners合并到$attrs中
-
移除$children:不再支持$children属性
组件通信方式概览
| 组件关系 | 传递方式 |
|---|---|
| 父传子 | props、v-model、$refs、默认插槽、具名插槽 |
| 子传父 | props、自定义事件、v-model、$parent、作用域插槽 |
| 祖孙组件 | $attrs、provide/inject |
| 兄弟/任意组件 | mitt、Pinia |
1. Props通信
Props是使用频率最高的通信方式,常用于父组件向子组件传递数据。
基本用法
javascript
// 父组件
<template>
<div class="father">
<h3>父组件</h3>
<h4>我的车: {{ car }}</h4>
<h4>儿子给的玩具: {{ toy }}</h4>
<Child :car="car" :getToy="getToy"/>
</div>
</template>
<script setup lang="ts">
import Child from './Child.vue'
import { ref } from "vue"
const car = ref('奔驰')
const toy = ref('')
function getToy(value: string) {
toy.value = value
}
</script>
// 子组件
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
const toy = ref('奥特曼')
defineProps(['car','getToy'])
</script>
关键点:
-
父传子时,属性值是非函数
-
子传父时,属性值是函数
-
使用
defineProps在子组件中接收props
2. 自定义事件
自定义事件常用于子组件向父组件通信。
基本概念
原生事件 vs 自定义事件:
-
原生事件:
-
事件名是特定的(click、mouseenter等)
-
事件对象$event包含事件相关信息
-
-
自定义事件:
-
事件名是任意名称
-
事件对象$event是emit时提供的数据
-
使用示例
javascript
// 父组件中绑定自定义事件
<Child @send-toy="toy = $event"/>
// 子组件中触发事件
const emit = defineEmits(['send-toy'])
emit('send-toy', '具体数据')
3. Mitt事件总线
Mitt是一个小巧的事件总线库,用于实现任意组件间通信。
安装和配置
bash
npm i mitt
javascript
// src/utils/emitter.ts import mitt from "mitt" const emitter = mitt() export default emitter
使用示例
javascript
// 接收数据的组件
import emitter from "@/utils/emitter"
import { onUnmounted } from "vue"
// 绑定事件
emitter.on('send-toy', (value) => {
console.log('send-toy事件被触发', value)
})
onUnmounted(() => {
// 解绑事件
emitter.off('send-toy')
})
// 提供数据的组件
import emitter from "@/utils/emitter"
function sendToy() {
// 触发事件
emitter.emit('send-toy', toy.value)
}
4. v-model双向绑定
v-model在Vue3中得到了显著增强,可以更方便地实现父子组件间的双向绑定。
v-model的本质
html
<!-- 使用v-model指令 --> <input type="text" v-model="userName"> <!-- v-model的本质 --> <input type="text" :value="userName" @input="userName = ($event.target as HTMLInputElement).value">
组件上的v-model
javascript
// 父组件
<AtguiguInput v-model="userName"/>
// 等价于
<AtguiguInput
:modelValue="userName"
@update:model-value="userName = $event"/>
// 子组件
<template>
<div class="box">
<input
type="text"
:value="modelValue"
@input="emit('update:model-value', $event.target.value)">
</div>
</template>
<script setup lang="ts">
defineProps(['modelValue'])
const emit = defineEmits(['update:model-value'])
</script>
多个v-model
javascript
<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>
5. $attrs属性透传
$attrs用于实现父组件向子组件的子组件通信(祖传孙)。
使用示例
javascript
// 父组件
<template>
<div class="father">
<Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/>
</div>
</template>
// 子组件
<template>
<div class="child">
<GrandChild v-bind="$attrs"/>
</div>
</template>
// 孙组件
<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(666)">点我更新A</button>
</div>
</template>
<script setup lang="ts">
defineProps(['a','b','c','d','x','y','updateA'])
</script>
注意:$attrs会自动排除props中声明的属性。
6. Provide/Inject依赖注入
provide/inject用于实现祖先组件向后代组件通信。
使用示例
javascript
// 祖先组件
<template>
<div class="father">
<h4>资产: {{ money }}</h4>
<h4>汽车: {{ car }}</h4>
<button @click="money += 1">资产+1</button>
<button @click="car.price += 1">汽车价格+1</button>
<Child/>
</div>
</template>
<script setup lang="ts">
import Child from './Child.vue'
import { ref, reactive, provide } from "vue"
let money = ref(100)
let car = reactive({
brand: '奔驰',
price: 100
})
function updateMoney(value: number) {
money.value += value
}
// 提供数据
provide('moneyContext', { money, updateMoney })
provide('car', car)
</script>
// 后代组件
<template>
<div class="grand-child">
<h3>我是孙组件</h3>
<h4>资产: {{ money }}</h4>
<h4>汽车: {{ car }}</h4>
<button @click="updateMoney(6)">点我</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue'
const { money, updateMoney } = inject('moneyContext', {
money: ref(0),
updateMoney: (x: number) => {}
})
const car = inject('car')
</script>
7. 插槽通信
插槽是Vue中强大的内容分发机制,也可以用于组件间通信。
默认插槽
javascript
// 父组件
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>
// 子组件
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot></slot>
</div>
</template>
具名插槽
javascript
// 父组件
<Category title="今日热门游戏">
<template v-slot:s1>
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</template>
<template #s2>
<a href="#">更多</a>
</template>
</Category>
// 子组件
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot name="s1"></slot>
<slot name="s2"></slot>
</div>
</template>
作用域插槽
作用域插槽允许子组件将数据传递给父组件。
javascript
// 父组件
<Game v-slot="params">
<ul>
<li v-for="g in params.games" :key="g.id">{{ g.name }}</li>
</ul>
</Game>
// 子组件
<template>
<div class="category">
<h2>今日游戏榜单</h2>
<slot :games="games" a="哈哈"></slot>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
let games = reactive([
{id:'asgdytsa01', name:'英雄联盟'},
{id:'asgdytsa02', name:'王者荣耀'},
{id:'asgdytsa03', name:'红色警戒'}
])
</script>
实际应用案例
案例1:模态框组件通信
javascript
// 父组件
<template>
<div class="app">
<button @click="showModal = true">打开模态框</button>
<Modal
:visible="showModal"
title="确认删除"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<p>确定要删除这个项目吗?</p>
</Modal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Modal from './components/Modal.vue'
const showModal = ref(false)
const handleConfirm = () => {
console.log('确认删除')
showModal.value = false
}
const handleCancel = () => {
console.log('取消删除')
showModal.value = false
}
</script>
// Modal组件
<template>
<div v-if="visible" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="$emit('cancel')">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<button @click="$emit('cancel')">取消</button>
<button @click="$emit('confirm')">确认</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
visible: Boolean,
title: String
})
defineEmits(['confirm', 'cancel'])
</script>
案例2:表单组件v-model
javascript
// 自定义输入组件
<template>
<div class="custom-input">
<label v-if="label">{{ label }}</label>
<input
:value="modelValue"
:placeholder="placeholder"
@input="$emit('update:modelValue', $event.target.value)"
@blur="$emit('blur')"
@focus="$emit('focus')"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<script setup lang="ts">
defineProps({
modelValue: String,
label: String,
placeholder: String,
error: String
})
defineEmits(['update:modelValue', 'blur', 'focus'])
</script>
// 使用自定义输入组件
<template>
<form class="form">
<CustomInput
v-model="form.username"
label="用户名"
placeholder="请输入用户名"
:error="errors.username"
/>
<CustomInput
v-model="form.email"
label="邮箱"
placeholder="请输入邮箱"
:error="errors.email"
/>
</form>
</template>
最佳实践与选择指南
如何选择合适的通信方式
-
父子组件通信
-
简单数据传递:使用props
-
表单类组件:使用v-model
-
需要访问子组件方法:使用$refs
-
内容分发:使用插槽
-
-
子父组件通信
-
简单事件:使用自定义事件
-
表单双向绑定:使用v-model
-
需要访问父组件:使用$parent(不推荐)
-
-
跨级组件通信
-
简单场景:使用$attrs
-
复杂数据共享:使用provide/inject
-
全局状态:使用Pinia
-
-
任意组件通信
-
事件驱动:使用mitt
-
状态管理:使用Pinia
-
性能考虑
-
避免过度使用provide/inject,它会使组件间的依赖关系不明确
-
对于频繁更新的数据,考虑使用Pinia进行状态管理
-
合理使用计算属性缓存计算结果
总结
Vue3提供了丰富而灵活的组件通信方式,每种方式都有其适用的场景。掌握这些通信方式,能够帮助您构建出结构清晰、维护性强的Vue应用。
关键要点:
-
Props:父子组件间最基本的数据传递方式
-
自定义事件:子组件向父组件通信的标准方式
-
v-model:简化表单类组件的双向绑定
-
插槽:强大的内容分发机制
-
Provide/Inject:跨层级组件通信的利器
-
Mitt:轻量级的事件总线解决方案
-
Pinia:现代化的状态管理库

更多推荐



所有评论(0)