通信方式可以根据组件之间的关系分为以下几大类:
一、父子组件通信(最常用)
这是最常见的数据流,包括父传子和子传父。
1. 父传子:props
最基础、最推荐的方式。父组件通过属性(Attribute)传递数据,子组件通过 defineProps 接收。
子组件 (Child.vue)
<template>
<div>
<h2>子组件</h2>
<p>收到来自父组件的消息:{{ message }}</p>
<p>数量:{{ count }}</p>
</div>
</template>
<script setup>
// 使用 defineProps 来声明 props
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
}
})
</script>
父组件 (Parent.vue)
<template>
<div>
<h1>父组件</h1>
<!-- 像传递 HTML 属性一样传递 props -->
<Child :message="parentMessage" :count="num" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMessage = ref('Hello from Parent!')
const num = ref(42)
</script>
2. 子传父:$emit / 自定义事件
子组件通过触发(emit)事件来向父组件传递数据。父组件监听这个事件并执行回调函数。
子组件 (Child.vue)
<template>
<button @click="sendMessageToParent">点击向父组件发送消息</button>
</template>
<script setup>
// 使用 defineEmits 来声明要触发的事件
const emit = defineEmits(['messageToParent'])
const sendMessageToParent = () => {
// 触发事件,并传递数据
emit('messageToParent', 'Hello Parent! from Child')
}
</script>
父组件 (Parent.vue)
<template>
<div>
<p>来自子组件的消息:{{ childMessage }}</p>
<!-- 监听子组件触发的事件 -->
<Child @message-to-parent="handleMessageFromChild" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childMessage = ref('')
const handleMessageFromChild = (msg) => {
childMessage.value = msg
}
</script>
3. 父组件直接访问子组件:ref 和 defineExpose
父组件可以通过 ref 获取子组件的实例,并调用其方法或访问其数据。
子组件 (Child.vue)
<template>
<div>{{ childData }}</div>
</template>
<script setup>
import { ref } from 'vue'
const childData = ref('子组件的数据')
const childMethod = () => {
console.log('子组件的方法被调用了')
}
// 使用 defineExpose 暴露数据或方法,父组件才能访问到
defineExpose({
childData,
childMethod
})
</script>
父组件 (Parent.vue)
<template>
<div>
<button @click="callChildMethod">调用子组件方法</button>
<Child ref="childRef" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 声明一个与子组件同名的 ref
const childRef = ref(null)
const callChildMethod = () => {
// 通过 .value 访问子组件实例,并调用其暴露出的方法或数据
childRef.value?.childMethod()
console.log(childRef.value?.childData)
}
</script>
二、兄弟组件或任意组件通信
当组件没有直接的父子关系时,上面的方法就很麻烦。这时需要"全局"或"跨级"的通信方案。
4. provide / inject(依赖注入)
适合深层嵌套的组件通信。祖先组件提供(provide)数据,任意层级的后代组件都可以注入(inject)使用。
祖先组件 (Ancestor.vue)
<template>
<div>
<Parent />
</div>
</template>
<script setup>
import { ref, provide } from 'vue'
import Parent from './Parent.vue'
const theme = ref('dark')
const user = ref({ name: 'Alice' })
// 提供数据,可以提供一个响应式的 ref 使其保持响应性
provide('themeColor', theme)
provide('userInfo', user)
</script>
后代组件 (Descendant.vue)
<template>
<div :class="theme">主题颜色是:{{ theme }}</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据,第二个参数是默认值(可选)
const theme = inject('themeColor', 'light') // 如果找不到 'themeColor',就使用 'light'
const user = inject('userInfo')
</script>
5. 全局事件总线(Event Bus)
Vue 3 中官方删除了 on, off 等实例方法,但我们可以使用第三方库(如 mitt 或 tiny-emitter)来实现一个轻量级的事件总线。
-
创建事件总线 (eventBus.js)
// 使用 mitt 库
import mitt from 'mitt'
const emitter = mitt()
export default emitter -
组件 A (发送事件)
<script setup> import emitter from './eventBus.js'const sendMessage = () => {
emitter.emit('global-event', { data: '任意数据' })
}
</script> -
组件 B (接收事件)
<script setup> import { onMounted, onUnmounted } from 'vue' import emitter from './eventBus.js'const handleEvent = (data) => {
console.log('收到消息:', data)
}// 组件挂载时监听事件
onMounted(() => {
emitter.on('global-event', handleEvent)
})// 组件卸载时取消监听,防止内存泄漏
onUnmounted(() => {
emitter.off('global-event', handleEvent)
})
</script>
6. 状态管理库(Pinia / Vuex)
对于复杂的大型应用,数据流可能非常混乱。使用状态管理库(Vue 3 官方推荐 Pinia)将共享状态抽离出来集中管理,是更专业、可预测的选择。
使用 Pinia 的简单示例:
-
创建 Store (stores/counter.js)
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } }) -
在任何组件中使用
<template>{{ store.count }}<button @click="store.increment()">+1</button> </template><script setup> import { useCounterStore } from '@/stores/counter' const store = useCounterStore() </script>
三、其他方式
• Vuex/Pinia 的替代方案:对于简单场景,也可以使用 reactive 创建一个全局响应式对象,然后到处导入使用(但缺乏像 Pinia 那样的调试工具和规范,不适合大型项目)。
• 使用 localStorage/sessionStorage:用于页面刷新后数据依然存在的场景,但非响应式,需要自己监听 storage 事件。
总结与选择建议
通信方式 关系 场景
props / $emit 父子组件 最常用,简单的数据传递
ref / defineExpose 父子组件 父组件需要直接调用子组件方法时
provide / inject 祖先-后代 深层嵌套组件,如主题、用户信息等
全局事件总线 任意组件 跨组件层级简单事件通知,中小型项目
Pinia (状态管理库) 任意组件 中大型项目,复杂的共享状态管理
简单口诀:
• 父子通信:props 下去,emit 上来。
• 父调子:用 ref。
• 祖孙通信:用 provide/inject。
• 任意通信:简单用事件总线,复杂用 Pinia。