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>
defineModel(Vue 3.4+)
Vue 3.4 引入了 defineModel 宏,让组件的双向绑定更加简洁。它是一个编译器宏,可以自动声明 prop 和对应的更新事件。
基本用法
<!-- MyInput.vue -->
<script setup>
// 自动声明 modelValue prop 和 update:modelValue 事件
const model = defineModel()
function updateValue(e) {
model.value = e.target.value
}
</script>
<template>
<input :value="model" @input="updateValue" />
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const text = ref('Hello')
</script>
<template>
<MyInput v-model="text" />
<p>{{ text }}</p>
</template>
与传统方式的对比
传统方式需要手动声明 prop 和 emit:
<!-- 传统方式 -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
使用 defineModel 更加简洁:
<!-- 使用 defineModel -->
<script setup>
const model = defineModel()
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>
命名 v-model
可以为 defineModel 指定名称:
<!-- MyForm.vue -->
<script setup>
// 指定 prop 名称为 "username"
const username = defineModel('username')
// 指定 prop 名称为 "email"
const email = defineModel('email')
</script>
<template>
<input v-model="username" placeholder="用户名" />
<input v-model="email" placeholder="邮箱" />
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const name = ref('')
const userEmail = ref('')
</script>
<template>
<MyForm
v-model:username="name"
v-model:email="userEmail"
/>
<p>用户名: {{ name }}</p>
<p>邮箱: {{ userEmail }}</p>
</template>
定义类型和默认值
<script setup>
// 指定类型
const count = defineModel({ type: Number })
// 带默认值
const name = defineModel({
type: String,
default: '匿名'
})
// 必填
const email = defineModel({
type: String,
required: true
})
// 多种类型
const id = defineModel({ type: [String, Number] })
</script>
处理修饰符
可以通过解构获取修饰符:
<script setup>
const [modelValue, modelModifiers] = defineModel()
// 对应 v-model.trim
if (modelModifiers.trim) {
console.log('用户使用了 .trim 修饰符')
}
</script>
值转换
使用 get 和 set 选项对值进行转换:
<script setup>
const [modelValue, modelModifiers] = defineModel({
// 读取时的转换
get(value) {
return value
},
// 写入时的转换
set(value) {
// 如果使用了 .trim 修饰符,自动去除空格
if (modelModifiers.trim) {
return value.trim()
}
return value
}
})
</script>
<template>
<input v-model="modelValue" />
</template>
<!-- Parent.vue -->
<template>
<!-- 自动去除首尾空格 -->
<MyInput v-model.trim="text" />
</template>
数字输入组件示例
<!-- NumberInput.vue -->
<script setup>
const model = defineModel({
type: Number,
default: 0,
set(value) {
// 确保值始终是数字
const num = Number(value)
return isNaN(num) ? 0 : num
}
})
function increment() {
model.value++
}
function decrement() {
model.value--
}
</script>
<template>
<div class="number-input">
<button @click="decrement">-</button>
<input type="number" v-model="model" />
<button @click="increment">+</button>
</div>
</template>
<style scoped>
.number-input {
display: flex;
align-items: center;
gap: 8px;
}
input {
width: 80px;
text-align: center;
}
</style>
开关组件示例
<!-- Toggle.vue -->
<script setup>
const model = defineModel({
type: Boolean,
default: false
})
</script>
<template>
<button
class="toggle"
:class="{ active: model }"
@click="model = !model"
>
{{ model ? '开' : '关' }}
</button>
</template>
<style scoped>
.toggle {
padding: 8px 16px;
border: 2px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
.toggle.active {
background: #42b883;
border-color: #42b883;
color: white;
}
</style>
<!-- 使用示例 -->
<script setup>
import { ref } from 'vue'
const isDark = ref(false)
const notifications = ref(true)
</script>
<template>
<div>
<p>深色模式: <Toggle v-model="isDark" /></p>
<p>通知: <Toggle v-model="notifications" /></p>
</div>
</template>
TypeScript 支持
<script setup lang="ts">
// 推断为 Ref<string | undefined>
const model = defineModel<string>()
// 必填,推断为 Ref<string>
const requiredModel = defineModel<string>({ required: true })
// 带修饰符类型
const [value, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
// modifiers 类型为 Record<'trim' | 'uppercase', true | undefined>
</script>
注意事项
- 默认值同步问题:如果子组件设置了默认值但父组件没有传递值,可能会导致父子组件值不同步:
<!-- Child.vue -->
<script setup>
// 父组件没有传递值时,model.value = 1
const model = defineModel({ default: 1 })
</script>
<!-- Parent.vue -->
<script setup>
const myRef = ref() // undefined
</script>
<template>
<!-- 父组件 myRef 是 undefined,子组件 model 是 1 -->
<Child v-model="myRef" />
</template>
- 响应式 Props 解构:Vue 3.5+ 支持 props 解构的响应式:
<script setup>
// Vue 3.5+: 解构的变量是响应式的
const { foo = 'default' } = defineProps(['foo'])
watchEffect(() => {
console.log(foo) // foo 变化时会重新执行
})
</script>
跨级组件通信
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 树形选择组件
- 实现一个全局消息通知系统
- 创建一个表单生成器组件
准备好进入下一章,学习插槽系统的内容了吗?