Vue 组件通信
Vue 组件通信是指在 Vue 应用中,组件之间传递数据和触发事件的机制。本章将详细介绍各种组件通信方式。
父子组件通信
父 -> 子:Props
父组件通过 Props 向子组件传递数据:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const message = ref('来自父组件的消息')
</script>
<template>
<Child :message="message" />
</template>
<!-- Child.vue -->
<script setup>
defineProps({
message: String
})
</script>
<template>
<p>{{ message }}</p>
</template>
Props 详细用法
Props 类型定义
// 基础类型
defineProps({
title: String,
count: Number,
isActive: Boolean
})
// 多个可能的类型
defineProps({
id: [String, Number]
})
// 必填的 Props
defineProps({
name: {
type: String,
required: true
}
})
// 带默认值的 Props
defineProps({
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => []
}
})
Props 验证
defineProps({
// 基础类型验证
name: {
type: String,
required: true,
validator: (value) => value.length > 0
},
// 自定义验证函数
status: {
type: String,
validator: (value) => ['pending', 'active', 'done'].includes(value)
}
})
子 -> 父:Events
子组件通过 emit 向父组件发送事件:
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['update', 'delete'])
function sendUpdate() {
emit('update', '新数据')
}
function handleDelete() {
emit('delete', 1) // 发送 id 为 1
}
</script>
<template>
<button @click="sendUpdate">更新</button>
<button @click="handleDelete">删除</button>
</template>
<!-- Parent.vue -->
<script setup>
function handleUpdate(newData) {
console.log('收到更新:', newData)
}
function handleDelete(id) {
console.log('删除:', id)
}
</script>
<template>
<Child
@update="handleUpdate"
@delete="handleDelete"
/>
</template>
v-model 与组件
组件可以使用 v-model 实现双向绑定:
<!-- MyInput.vue -->
<script setup>
defineProps({
modelValue: String
})
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const text = ref('Hello')
</script>
<template>
<MyInput v-model="text" />
<p>{{ text }}</p>
</template>
多个 v-model
<!-- MyForm.vue -->
<script setup>
defineProps({
name: String,
email: String
})
defineEmits(['update:name', 'update:email'])
</script>
<template>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
/>
<input
:value="email"
@input="$emit('update:email', $event.target.value)"
/>
</template>
<!-- Parent.vue -->
<template>
<MyForm
v-model:name="name"
v-model:email="email"
/>
</template>
跨级组件通信
Provide / Inject
祖先组件向所有后代组件传递数据,无论层级有多深:
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
provide('appName', 'My App')
</script>
<template>
<ChildComponent />
</template>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const appName = inject('appName', 'Default App') // 默认值
</script>
<template>
<p>主题: {{ theme }}</p>
<p>应用: {{ appName }}</p>
</template>
Provide / Inject 与响应式
为了保持注入值的响应式,可以使用 ref 或 computed:
<!-- 祖先组件 -->
<script setup>
import { provide, ref, computed } from 'vue'
const user = ref({ name: '张三', age: 25 })
// 提供响应式对象
provide('user', user)
// 或者提供只读的计算属性
provide('userName', computed(() => user.value.name))
</script>
兄弟组件通信
通过父组件中转
两个兄弟组件通过父组件进行数据传递:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import SiblingA from './SiblingA.vue'
import SiblingB from './SiblingB.vue'
const sharedData = ref('')
function handleUpdate(data) {
sharedData.value = data
}
</script>
<template>
<SiblingA @update="handleUpdate" />
<SiblingB :data="sharedData" />
</template>
事件总线(Event Bus)
对于简单的跨组件通信,可以使用事件总线模式:
// eventBus.js
import { reactive } from 'vue'
export const eventBus = reactive({
events: {},
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
},
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
},
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(...args))
}
}
})
<!-- ComponentA.vue -->
<script setup>
import { eventBus } from './eventBus'
eventBus.emit('message', 'Hello')
</script>
<!-- ComponentB.vue -->
<script setup>
import { eventBus } from './eventBus'
eventBus.on('message', (msg) => {
console.log('收到消息:', msg)
})
</script>
示例:购物车组件
<!-- Cart.vue - 购物车主组件 -->
<script setup>
import { ref, computed } from 'vue'
import CartItem from './CartItem.vue'
const items = ref([
{ id: 1, name: '苹果', price: 5, quantity: 2 },
{ id: 2, name: '香蕉', price: 3, quantity: 1 },
{ id: 3, name: '橙子', price: 4, quantity: 3 }
])
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})
function updateQuantity(id, quantity) {
const item = items.value.find(item => item.id === id)
if (item) {
item.quantity = Math.max(0, quantity)
}
}
function removeItem(id) {
const index = items.value.findIndex(item => item.id === id)
if (index > -1) {
items.value.splice(index, 1)
}
}
</script>
<template>
<div class="cart">
<h2>购物车</h2>
<div v-if="items.length === 0">购物车为空</div>
<CartItem
v-for="item in items"
:key="item.id"
:item="item"
@update-quantity="updateQuantity"
@remove="removeItem"
/>
<div v-if="items.length > 0" class="total">
总计: ¥{{ totalPrice }}
</div>
</div>
</template>
<!-- CartItem.vue -->
<script setup>
defineProps({
item: Object
})
const emit = defineEmits(['updateQuantity', 'remove'])
</script>
<template>
<div class="cart-item">
<span class="name">{{ item.name }}</span>
<span class="price">¥{{ item.price }}</span>
<div class="quantity">
<button @click="emit('updateQuantity', item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="emit('updateQuantity', item.id, item.quantity + 1)">+</button>
</div>
<span class="subtotal">¥{{ item.price * item.quantity }}</span>
<button class="remove" @click="emit('remove', item.id)">删除</button>
</div>
</template>
<style scoped>
.cart-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.name { flex: 2; }
.price { flex: 1; }
.quantity { flex: 1; display: flex; align-items: center; gap: 5px; }
.subtotal { flex: 1; }
.remove { background: #ff4444; color: white; border: none; padding: 5px 10px; }
</style>
小结
本章我们详细学习了 Vue 组件通信的各种方式:
- 父子通信:Props 向下传递,Events 向上传递
- v-model:双向绑定的简洁写法
- Provide / Inject:跨级组件通信
- 事件总线:简单的跨组件通信模式
- 购物车示例:综合运用组件通信
练习
- 创建一个 TreeSelect 树形选择组件
- 实现一个全局消息通知系统
- 创建一个表单生成器组件
准备好进入下一章,学习插槽系统的内容了吗?