Vue.js 组件通信
组件通信是 Vue 开发中的核心概念。本章介绍父子组件、兄弟组件以及跨层级组件之间的通信方式。
父子组件通信
Props(父传子)
父组件通过 props 向子组件传递数据。
父组件:
vue
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const message = ref('Hello from Parent')
const count = ref(10)
</script>
<template>
<div>
<h2>父组件</h2>
<ChildComponent :message="message" :count="count" />
</div>
</template>子组件 (ChildComponent.vue):
vue
<script setup>
defineProps({
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
</script>
<template>
<div>
<h3>子组件</h3>
<p>接收到的消息:{{ message }}</p>
<p>接收到的数字:{{ count }}</p>
</div>
</template>Emits(子传父)
子组件通过触发事件向父组件传递数据。
子组件:
vue
<script setup>
const emit = defineEmits(['update', 'delete'])
function handleClick() {
emit('update', { id: 1, name: '更新的数据' })
}
function handleDelete() {
emit('delete', 1)
}
</script>
<template>
<div>
<button @click="handleClick">更新</button>
<button @click="handleDelete">删除</button>
</div>
</template>父组件:
vue
<script setup>
import ChildComponent from './ChildComponent.vue'
function handleUpdate(data) {
console.log('收到更新:', data)
}
function handleDelete(id) {
console.log('删除 ID:', id)
}
</script>
<template>
<ChildComponent
@update="handleUpdate"
@delete="handleDelete"
/>
</template>v-model(双向绑定)
v-model 是 props 和 emits 的语法糖。
子组件:
vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function updateValue(event) {
emit('update:modelValue', event.target.value)
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>父组件:
vue
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const text = ref('')
</script>
<template>
<div>
<CustomInput v-model="text" />
<p>输入的内容:{{ text }}</p>
</div>
</template>多个 v-model
vue
<!-- 子组件 -->
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<div>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="姓"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="名"
/>
</div>
</template>
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import NameInput from './NameInput.vue'
const firstName = ref('')
const lastName = ref('')
</script>
<template>
<NameInput v-model:first-name="firstName" v-model:last-name="lastName" />
<p>全名:{{ firstName }} {{ lastName }}</p>
</template>Provide / Inject(跨层级通信)
用于祖先组件向后代组件传递数据,无论层级多深。
祖先组件:
vue
<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = ref('dark')
const user = ref({ name: '张三', role: 'admin' })
// 提供数据
provide('theme', theme)
provide('user', user)
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>
<template>
<div>
<button @click="toggleTheme">切换主题</button>
<ChildComponent />
</div>
</template>后代组件(任意层级):
vue
<script setup>
import { inject } from 'vue'
// 注入数据
const theme = inject('theme')
const user = inject('user')
</script>
<template>
<div :class="`theme-${theme}`">
<p>当前主题:{{ theme }}</p>
<p>用户:{{ user.name }} ({{ user.role }})</p>
</div>
</template>提供默认值
vue
<script setup>
import { inject } from 'vue'
// 如果没有提供,使用默认值
const theme = inject('theme', 'light')
const config = inject('config', () => ({ mode: 'default' }))
</script>模板引用(Template Refs)
父组件可以通过 ref 直接访问子组件的实例。
子组件:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 暴露给父组件
defineExpose({
count,
increment
})
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>父组件:
vue
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
function accessChild() {
console.log('子组件的 count:', childRef.value.count)
childRef.value.increment()
}
</script>
<template>
<div>
<ChildComponent ref="childRef" />
<button @click="accessChild">访问子组件</button>
</div>
</template>事件总线(Event Bus)
适用于非父子组件通信(Vue 3 推荐使用状态管理或 provide/inject)。
创建事件总线:
javascript
// eventBus.js
import { ref } from 'vue'
const bus = ref(new Map())
export function useEventBus() {
function emit(event, ...args) {
bus.value.get(event)?.forEach(callback => callback(...args))
}
function on(event, callback) {
if (!bus.value.has(event)) {
bus.value.set(event, [])
}
bus.value.get(event).push(callback)
}
function off(event, callback) {
const callbacks = bus.value.get(event)
if (callbacks) {
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
return { emit, on, off }
}使用事件总线:
vue
<!-- 组件 A -->
<script setup>
import { useEventBus } from './eventBus'
const { emit } = useEventBus()
function sendMessage() {
emit('message', 'Hello from Component A')
}
</script>
<template>
<button @click="sendMessage">发送消息</button>
</template>
<!-- 组件 B -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useEventBus } from './eventBus'
const { on, off } = useEventBus()
function handleMessage(message) {
console.log('收到消息:', message)
}
onMounted(() => {
on('message', handleMessage)
})
onUnmounted(() => {
off('message', handleMessage)
})
</script>
<template>
<div>等待消息...</div>
</template>实战示例:购物车系统
App.vue(父组件):
vue
<script setup>
import { ref, provide } from 'vue'
import ProductList from './ProductList.vue'
import ShoppingCart from './ShoppingCart.vue'
const cart = ref([])
function addToCart(product) {
const existingItem = cart.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
cart.value.push({ ...product, quantity: 1 })
}
}
function removeFromCart(productId) {
cart.value = cart.value.filter(item => item.id !== productId)
}
function updateQuantity(productId, quantity) {
const item = cart.value.find(item => item.id === productId)
if (item) {
item.quantity = quantity
}
}
// 提供购物车数据和方法
provide('cart', {
items: cart,
addToCart,
removeFromCart,
updateQuantity
})
</script>
<template>
<div class="app">
<h1>在线商店</h1>
<div class="container">
<ProductList />
<ShoppingCart />
</div>
</div>
</template>
<style scoped>
.container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
padding: 20px;
}
</style>ProductList.vue:
vue
<script setup>
import { ref, inject } from 'vue'
const products = ref([
{ id: 1, name: 'iPhone 14', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 12999 },
{ id: 3, name: 'iPad Air', price: 4399 },
{ id: 4, name: 'AirPods Pro', price: 1999 }
])
const { addToCart } = inject('cart')
</script>
<template>
<div class="product-list">
<h2>商品列表</h2>
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
</div>
</template>
<style scoped>
.product-list {
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.product-card {
padding: 15px;
margin-bottom: 10px;
background-color: white;
border-radius: 4px;
}
.price {
font-size: 20px;
font-weight: bold;
color: #42b983;
margin: 10px 0;
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>ShoppingCart.vue:
vue
<script setup>
import { inject, computed } from 'vue'
const { items, removeFromCart, updateQuantity } = inject('cart')
const total = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})
</script>
<template>
<div class="shopping-cart">
<h2>购物车</h2>
<div v-if="items.length === 0" class="empty">
购物车是空的
</div>
<div v-else>
<div v-for="item in items" :key="item.id" class="cart-item">
<div class="item-info">
<h4>{{ item.name }}</h4>
<p>¥{{ item.price }}</p>
</div>
<div class="item-actions">
<input
type="number"
:value="item.quantity"
@input="updateQuantity(item.id, Number($event.target.value))"
min="1"
/>
<button @click="removeFromCart(item.id)" class="remove-btn">
删除
</button>
</div>
</div>
<div class="total">
<strong>总计:¥{{ total }}</strong>
</div>
<button class="checkout-btn">结算</button>
</div>
</div>
</template>
<style scoped>
.shopping-cart {
padding: 20px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
position: sticky;
top: 20px;
}
.empty {
text-align: center;
color: #999;
padding: 40px 0;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.item-actions {
display: flex;
gap: 10px;
align-items: center;
}
.item-actions input {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.remove-btn {
padding: 5px 10px;
background-color: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.total {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #ddd;
text-align: right;
font-size: 18px;
}
.checkout-btn {
width: 100%;
margin-top: 15px;
padding: 12px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
</style>总结
- Props:父组件向子组件传递数据
- Emits:子组件向父组件发送事件
- v-model:实现双向绑定
- Provide/Inject:跨层级组件通信
- Template Refs:父组件直接访问子组件
- Event Bus:兄弟组件或非父子组件通信
选择合适的通信方式:
- 父子组件:优先使用 Props 和 Emits
- 跨层级:使用 Provide/Inject
- 复杂状态:使用 Vuex 或 Pinia
下一步
接下来学习 动态组件,了解如何动态切换组件。