Vue-组件通信全攻略
前言
在 Vue 开发中,组件通信是构建复杂应用的基础。随着 Vue 3 的普及,通信方式发生了不少变化(如 defineProps 的引入、EventBus 的退场)。本文将对比 Vue 2 与 Vue 3,带你梳理最常用的 5 种通信方案。
一、 父子组件通信:最基础的单向数据流
这是最常用的通信方式,遵循“Props 向下传递,Emit 向上通知”的原则。
1. Vue 2 经典写法
-
接收:使用
props选项。 -
发送:使用
this.$emit。
2. Vue 3 + TS 标准写法
在 Vue 3 <script setup> 中,我们使用 defineProps 和 defineEmits。
父组件:Parent.vue
<template>
<ChildComponent :id="currentId" @childEvent="handleChild" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './Child.vue';
const currentId = ref<string>('001');
const handleChild = (msg: string) => {
console.log('接收到子组件消息:', msg);
};
</script>
子组件:Child.vue
<script setup lang="ts">
// 使用 TS 类型定义 Props
const props = defineProps<{
id: string
}>();
// 使用 TS 定义 Emits,具备更好的类型检查
const emit = defineEmits<{
(e: 'childEvent', args: string): void;
}>();
const sendMessage = () => {
emit('childEvent', '这是来自子组件的参数');
};
</script>
二、 跨级调用:通过 Ref 访问实例
有时父组件需要直接调用子组件的内部方法。
1. Vue 2 模式
直接通过 this.$refs.childRef.someMethod() 调用。
2. Vue 3 模式(显式暴露)
Vue 3 的组件默认是关闭的。如果父组件想访问子组件的方法,子组件必须使用 defineExpose。
子组件:Child.vue
<script setup lang="ts">
const childFunc = () => {
console.log('子组件方法被调用');
};
// 必须手动暴露,父组件才能访问
defineExpose({
childFunc
});
</script>
父组件:Parent.vue
<template>
<Child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
// 这里的类型定义有助于获得代码提示
const childRef = ref<InstanceType<typeof Child> | null>(null);
onMounted(() => {
childRef.value?.childFunc();
});
</script>
三、 非父子组件通信:事件总线 (EventBus)
1. Vue 2 做法
利用一个新的 Vue 实例作为中央调度器。
import Vue from 'vue';
export const EventBus = new Vue();
// 组件 A 发送
EventBus.$emit('event', data);
// 组件 B 接收
EventBus.$on('event', (data) => { ... });
2. Vue 3 重要变更
Vue 3 官方已移除了 $on、$off 和 $once 方法,因此不再支持直接通过 Vue 实例创建 EventBus。
-
官方推荐方案:使用第三方库
mitt或tiny-emitter。 -
补充:如果逻辑简单,可以使用 Vue 3 的
provide/inject实现跨级通信。
provide / inject 示例:
- 祖先组件:提供数据 (
App.vue)
<template>
<div class="ancestor">
<h1>祖先组件</h1>
<p>当前主题:{{ theme }}</p>
<Middle />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue';
import Middle from './Middle.vue';
// 1. 定义响应式数据
const theme = ref<'light' | 'dark'>('light');
// 2. 定义修改数据的方法(推荐在提供者内部定义,保证数据流向清晰)
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
// 3. 注入 key 和对应的值/方法
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
-
中间组件:无需操作 (
Middle.vue)中间组件不需要显式接收
theme,直接透传即可 -
后代组件:注入并使用 (
DeepChild.vue)
<template>
<div class="descendant">
<h3>深层子组件</h3>
<p>接收到的主题:{{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
// 使用 inject 获取,第二个参数为默认值(可选)
const theme = inject('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
四、 集中式状态管理:Vuex 与 Pinia
当应用变得庞大,组件间的关系交织成网时,我们需要一个“单一事实来源”。
-
Vuex:Vue 2 时代的标准。基于 Mutation(同步)和 Action(异步)。
-
Pinia:Vue 3 的官方推荐。
- 优势:更完美的 TS 支持、没有 Mutation 的繁琐逻辑、极其轻量。
-
核心:
state、getters、actions。
Pinia 示例:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 18
}),
actions: {
updateName(newName: string) {
this.name = newName;
}
}
});
五、 总结与纠错
-
安全性建议:在使用
defineExpose时,尽量只暴露必要的接口,遵循最小暴露原则。 -
EventBus 警示:Vue 3 开发者请注意,不要再尝试使用
new Vue()来做事件总线,应当转向 Pinia 或全局状态。