跳到主要内容

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 与响应式

为了保持注入值的响应式,可以使用 refcomputed

<!-- 祖先组件 -->
<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 组件通信的各种方式:

  1. 父子通信:Props 向下传递,Events 向上传递
  2. v-model:双向绑定的简洁写法
  3. Provide / Inject:跨级组件通信
  4. 事件总线:简单的跨组件通信模式
  5. 购物车示例:综合运用组件通信

练习

  1. 创建一个 TreeSelect 树形选择组件
  2. 实现一个全局消息通知系统
  3. 创建一个表单生成器组件

准备好进入下一章,学习插槽系统的内容了吗?