VUE3-组件间通讯

前言

在 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
<!-- 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
<!-- Parent.vue -->
<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 不适用于响应式更新,除非你注入的是 refreactive 对象。

全局状态管理

Pinia(推荐)或 Vuex

对于复杂应用,建议使用状态管理库。

Vue 3 官方推荐 Pinia

安装

1
npm install pinia

注册Pinia

1
2
3
4
5
6
7
8
9
// main.js
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
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
// ComponentA.vue
emitter.emit('custom-event', data)

// ComponentB.vue
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
<!-- Child.vue -->
<script setup>
import { defineExpose } from 'vue'

function someMethod() {
console.log('Called from parent')
}

defineExpose({ someMethod })
</script>

总结建议

场景 推荐方式
父子通信 Props + Emits
祖先→后代 provide / inject
全局共享状态 Pinia
兄弟通信 Pinia 或 通过父组件中转
紧耦合交互(如表单控件) ref + defineExpose