Vue 组件通信主要分为三大类:
- 父子组件通信 (最常见)
- 兄弟/跨级组件通信
- 全局状态管理
一、父子组件通信
1. 父传子:Props
这是最基础、最常用的方式。父组件通过属性(Attributes)向下传递数据,子组件通过 props 选项来声明接收。
子组件 (ChildComponent.vue):
<script setup>
// 1. 使用 defineProps 编译器宏来声明 props
const props = defineProps({
title: String, // 基础类型检查
userInfo: Object, // 复杂类型检查
likes: {
type: Number,
default: 0 // 默认值
},
isPublished: Boolean
})
// 在脚本中使用 props
console.log(props.title)
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Likes: {{ likes }}</p>
</div>
</template>
父组件 (ParentComponent.vue):
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const postTitle = ref('Hello Vue!')
const author = ref({ name: 'Alice', age: 25 })
const count = ref(10)
</script>
<template>
<!-- 2. 通过属性传递数据 -->
<ChildComponent
:title="postTitle"
:user-info="author"
:likes="count"
is-published
/>
</template>
2. 子传父:自定义事件 (Emits)
子组件通过触发(Emit)自定义事件,向父组件传递数据。
子组件 (ChildComponent.vue):
<script setup>
// 1. 声明它可能触发的自定义事件
const emit = defineEmits(['enlarge-text', 'submit'])
function handleClick() {
// 2. 触发事件,并传递数据
emit('enlarge-text', 2) // 传递一个数字
// emit('submit', { username: 'alice', password: 'secret' }) // 传递一个对象
}
</script>
<template>
<button @click="handleClick">Enlarge Text</button>
<!-- 也可以直接在模板中触发 -->
<!-- <button @click="$emit('enlarge-text', 2)">Enlarge Text</button> -->
</template>
父组件 (ParentComponent.vue):
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const fontSize = ref(1)
const textSize = ref(1)
// 3. 监听子组件触发的事件
function onEnlargeText(scale) {
fontSize.value += scale
}
</script>
<template>
<div :style="{ fontSize: fontSize + 'em' }">
<!-- 4. 使用 v-on 或 @ 来监听子组件发出的事件 -->
<ChildComponent @enlarge-text="onEnlargeText" />
<!-- 或者使用内联方式 -->
<ChildComponent @enlarge-text="textSize += $event" />
</div>
</template>
3. 父组件直接访问子组件:模板引用 (Template Refs)
父组件通过 ref 属性获取子组件实例或 DOM 元素,从而直接调用其方法或访问其数据。
子组件 (ChildComponent.vue):
<script setup>
import { ref } from 'vue'
const input = ref(null)
const message = ref('Hello from Child!')
// 暴露给父组件的方法或属性
defineExpose({
message,
focusInput: () => {
input.value?.focus()
}
})
</script>
<template>
<input ref="input" />
</template>
父组件 (ParentComponent.vue):
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 1. 创建一个同名的 ref
const childRef = ref(null)
onMounted(() => {
// 2. 子组件挂载后,可以通过 ref 访问
console.log(childRef.value.message) // 'Hello from Child!'
childRef.value.focusInput() // 调用子组件的方法
})
</script>
<template>
<!-- 3. 在子组件上绑定 ref -->
<ChildComponent ref="childRef" />
</template>
二、兄弟/跨级组件通信
当组件层级关系很深时,使用 Props 逐层传递会非常繁琐(称为“Prop 逐级透传”问题)。这时有以下解决方案:
1. Provide / Inject (提供/注入)
祖先组件提供(Provide)数据,任意后代组件都可以注入(Inject)使用,无需通过中间每一层传递。
祖先组件 (Ancestor.vue):
<script setup>
import { provide, ref } from 'vue'
import ParentComponent from './ParentComponent.vue'
const theme = ref('dark')
// 提供静态数据
provide('app-theme', 'dark') // key: 字符串,value: 任何值
// 提供响应式数据,使注入方能响应变化
provide('theme', theme)
// 提供修改数据的方法
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide('toggleTheme', toggleTheme)
</script>
<template>
<ParentComponent />
</template>
后代组件 (Descendant.vue) - 可以是任意深层级的子组件:
<script setup>
import { inject } from 'vue'
// 注入数据,如果没有找到,则使用默认值 'light'
const theme = inject('theme', 'light')
// 注入方法
const toggleTheme = inject('toggleTheme')
// 注入非响应式数据
const appTheme = inject('app-theme')
</script>
<template>
<button @click="toggleTheme">Toggle Theme (Current: {{ theme }})</button>
</template>
2. 全局事件总线 (Event Bus) - (Vue 3 较少使用)
创建一个全局的 Vue 实例来充当中央事件总线,任何组件都可以在上面触发和监听事件。
eventBus.js:
import mitt from 'mitt' // 一个轻量的第三方事件库
const emitter = mitt()
export default emitter
组件 A (触发事件):
<script setup>
import emitter from './eventBus'
function sendMessage() {
emitter.emit('user-message', { text: 'Hello from Component A!' })
}
</script>
组件 B (监听事件):
<script setup>
import { onUnmounted } from 'vue'
import emitter from './eventBus'
// 监听事件
emitter.on('user-message', (message) => {
console.log(message.text)
})
// 组件卸载时取消监听,防止内存泄漏
onUnmounted(() => {
emitter.off('user-message')
})
</script>
注意:在大型项目中,事件总线容易导致事件流混乱,难以维护,通常更推荐使用 Pinia。
三、全局状态管理 (Pinia)
对于复杂的应用数据,多个不相关的组件需要共享状态时,使用全局状态管理库是最佳实践。Vue 官方推荐 Pinia(替代了旧的 Vuex)。
store/counter.js:
import { defineStore } from 'pinia'
// 定义一个 store
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
任何组件中都可以使用:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 在 setup 中访问 store
const counterStore = useCounterStore()
</script>
<template>
<div>{{ counterStore.count }}</div>
<div>{{ counterStore.doubleCount }}</div>
<button @click="counterStore.increment()">Increment</button>
<!-- 也可以使用 $patch 直接修改 -->
<button @click="counterStore.$patch({ count: 10 })">Set to 10</button>
</template>
总结与选择建议
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props / Emits | 父子组件 | 简单、直观、明确的数据流 | 深层级组件传递麻烦 |
| Template Refs | 父操作子 | 需要直接调用子组件方法或访问 DOM 时 | 破坏了组件的封装性,应谨慎使用 |
| Provide/Inject | 跨多级组件 | 避免 Prop 逐级透传,祖先和后代解耦 | 数据流变得不明确,不利于调试 |
| Event Bus | 任意组件 | 简单快速,任意组件通信 | 事件流难以追踪,大型项目中易混乱 |
| Pinia (推荐) | 全局状态共享 | 集中管理,响应式,DevTools 支持,类型安全 | 需要学习额外的概念,对于简单场景稍重 |
简单选择指南:
- 父子通信:首选 Props 和 Emits。
- 父需直接调用子:使用 Template Refs。
- 爷爷传孙子(或更深) :使用 Provide / Inject。
- 完全不相关的组件或复杂全局状态:使用 Pinia。