Vue-组件通信全攻略

0 阅读2分钟

前言

在 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> 中,我们使用 definePropsdefineEmits

父组件: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。

  • 官方推荐方案:使用第三方库 mitttiny-emitter
  • 补充:如果逻辑简单,可以使用 Vue 3 的 provide / inject 实现跨级通信。

provide / inject 示例:

  1. 祖先组件:提供数据 (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>
  1. 中间组件:无需操作 (Middle.vue)

    中间组件不需要显式接收 theme,直接透传即可

  2. 后代组件:注入并使用 (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 的繁琐逻辑、极其轻量。
    • 核心stategettersactions

Pinia 示例:

import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '张三',
    age: 18
  }),
  actions: {
    updateName(newName: string) {
      this.name = newName;
    }
  }
});

五、 总结与纠错

  1. 安全性建议:在使用 defineExpose 时,尽量只暴露必要的接口,遵循最小暴露原则。
  2. EventBus 警示:Vue 3 开发者请注意,不要再尝试使用 new Vue() 来做事件总线,应当转向 Pinia 或全局状态。