前言
在 Vue 3 中,组件之间的通信方式多种多样,具体选择取决于组件之间的关系(父子、兄弟、跨层级等)以及应用的复杂度。
以下是常见的几种通信方式:
父→子:Props
这是最基础、最常用的通信方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!-- Parent.vue --> <template> <Child :message="parentMsg" /> </template>
<script setup> import Child from './Child.vue' const parentMsg = 'Hello from parent' </script> <!-- Child.vue --> <template> <p>{{ message }}</p> </template>
<script setup> defineProps({ message: String }) </script>
|
子→父:Emits
子组件通过 emit 触发事件,父组件监听。
子组件
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- Child.vue --> <template> <button @click="notifyParent">Click me</button> </template>
<script setup> const emit = defineEmits(['update-message']) function notifyParent() { emit('update-message', 'New message from child') } </script>
|
父组件
1 2 3 4 5 6 7 8 9 10 11
| <template> <Child @update-message="handleMessage" /> </template>
<script setup> import Child from './Child.vue' function handleMessage(msg) { console.log(msg) } </script>
|
定义多个事件
1 2 3 4 5 6 7
| <script setup> const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() { emit('submit') } </script>
|
TS中定义多个事件
1 2 3 4 5 6
| <script setup lang="ts"> const emit = defineEmits<{ (e: 'pageSubmit', value: number): void (e: 'drawerClose'): void }>() </script>
|
兄弟组件通信
兄弟组件通信:通过共同父组件中转
兄弟之间不能直接通信,通常借助父组件作为“中介”:
- 兄弟 A → 父(emit)
- 父 → 兄弟 B(props)
祖先→后代
使用 provide / inject
适用于祖先组件向深层后代传递数据,避免逐层 props 透传。
1 2 3 4 5 6 7 8 9 10 11
| <!-- Ancestor.vue --> <script setup> import { provide, ref } from 'vue' const theme = ref('dark') provide('theme', theme) </script> <!-- DeepChild.vue --> <script setup> import { inject } from 'vue' const theme = inject('theme') </script>
|
注意:
provide/inject 不适用于响应式更新,除非你注入的是 ref 或 reactive 对象。
全局状态管理
Pinia(推荐)或 Vuex
对于复杂应用,建议使用状态管理库。
Vue 3 官方推荐 Pinia。
安装
注册Pinia
1 2 3 4 5 6 7 8 9
| import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue'
const app = createApp(App) const pinia = createPinia() app.use(pinia) app.mount('#app')
|
stores/common.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { ref } from 'vue' import { defineStore } from 'pinia'
export const useCommonStore = defineStore('common', () => { const loading = ref<boolean>(false)
function showLoading() { loading.value = true }
function hideLoading() { loading.value = false }
return { loading, showLoading, hideLoading } })
|
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div> <a-spin tip="数据获取中..." :spinning="loading"> </a-spin> </div> </template>
<script setup> import { useCommonStore } from '@/stores/common' import { storeToRefs } from 'pinia'
const commonStore = useCommonStore() const { loading } = storeToRefs(commonStore) </script>
|
其他页面使用
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div> <button @click="showLoading">显示</button> <button @click="hideLoading">隐藏</button> </div> </template>
<script setup> import { useCommonStore } from '@/stores/common' const commonStore = useCommonStore() const { showLoading, hideLoading } = commonStore </script>
|
事件总线
不推荐,但可行
Vue 3 移除了 $on, $emit 实例方法,但可用第三方库(如 mitt)实现事件总线:
1 2 3 4 5 6 7 8
| import mitt from 'mitt' export const emitter = mitt()
emitter.emit('custom-event', data)
emitter.on('custom-event', (data) => { ... })
|
注意:
事件总线容易导致代码难以维护,仅适合小型项目或临时方案。
调用子组件方法
模板引用(ref)访问子组件实例。
子组件使用defineExpose导出属性或方法。
父组件可通过 ref 直接调用子组件的方法或访问其数据(不推荐频繁使用,破坏封装性)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!-- Parent.vue --> <template> <Child ref="childRef" /> </template>
<script setup> import { ref, onMounted } from 'vue' import Child from './Child.vue'
const childRef = ref(null) onMounted(() => { childRef.value.someMethod() }) </script>
|
子组件
1 2 3 4 5 6 7 8 9 10
| <script setup> import { defineExpose } from 'vue'
function someMethod() { console.log('Called from parent') }
defineExpose({ someMethod }) </script>
|
总结建议
| 场景 |
推荐方式 |
| 父子通信 |
Props + Emits |
| 祖先→后代 |
provide / inject |
| 全局共享状态 |
Pinia |
| 兄弟通信 |
Pinia 或 通过父组件中转 |
| 紧耦合交互(如表单控件) |
ref + defineExpose |