Vue3 组件通信的七种方式与选型

发布于2026-10-16 16:34 阅读15次 同一个需求换了三套通信方案,每次都要动一片代码。本文把 Vue 3 常用的七种通信方式按使用成本从低到高排列,逐个说明适用场景与易错点,最后给出一条六步选型决策路径,帮助在 props、provide、Pinia 之间做出不返工的选择。
## 问题
一个 Vue 3 后台项目,父子孙三层组件,需要把一个表单的校验状态同步给最外层。第一版用了 `provide/inject` 硬传,第二版改成事件总线,第三版上了 Pinia——每换一次方案都要动一片代码。
组件通信这件事,方法不少,但选错了维护成本很高。这篇把常用方式和适用场景捋清楚。
## 七种方式速查
| 方式 | 方向 | 适用场景 |
|---|---|---|
| props / emit | 父 → 子 | 最常用,首选 |
| v-model | 父 ↔ 子 | 表单类组件封装 |
| ref 模板引用 | 父 → 子 | 命令式调用子组件方法 |
| provide / inject | 祖先 → 后代 | 跨层配置、主题、国际化 |
| 事件总线 | 任意 | 慎用,难追踪 |
| Pinia / Vuex | 任意 | 跨路由共享状态 |
| 路由参数 / query | 页面间 | 跨页面传递 |
## 1. props / emit
永远的第一选择。约定清晰,数据流单向,DevTools 里能直接看到组件树上的 props。
```vue
<script setup>
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [v: string] }>();
</script>
```
原则:**能用 props 解决的,不要想别的。**
## 2. v-model
`v-model` 本质是 `modelValue` + `update:modelValue` 的语法糖。封装输入框、开关、选择器这类组件时用它,父组件写法最自然。
一个组件可以绑定多个 v-model:
```vue
<MyForm v-model="name" v-model:page="pageNo" />
```
对应子组件里 `modelValue` 和 `page` 两个 props。
## 3. 模板 ref
需要命令式调用子组件方法时用,比如"点击父组件按钮,让子组件的表格刷新":
```ts
const tableRef = ref<InstanceType<typeof MyTable>>();
tableRef.value?.refresh();
```
注意子组件要用 `defineExpose` 显式暴露,否则默认只能拿到 `$el`。
## 4. provide / inject
跨层传递,避免一层层 props 钻探。适合主题、语言、全局配置这类"注入型"数据。
```ts
// 祖先
provide('tableConfig', { size: 'small' });
// 后代
const cfg = inject<{ size: string }>('tableConfig');
```
坑:**inject 的值是普通对象时不会自动响应式**。要传响应式数据就用 `ref` 或 `computed` 包一层,或者用 `provide` 传 `readonly` 的 reactive。
另外 inject 没有默认值保护时要写第二个参数,否则找不到就是 undefined,报错很晚才发现。
## 5. 事件总线
用 `mitt` 之类的小库实现跨组件通信。看起来方便,实际是灾难——发出去了不知道谁在收,出问题无法追踪。
只在两种情况下用:组件关系极远、且通信是一次性的(比如"全局通知弹窗关闭")。**常规业务别碰。**
## 6. Pinia
跨路由共享状态的标准答案。用户信息、权限、字典数据、购物车这类放这里。
```ts
const user = useUserStore();
user.name = 'x'; // 直接改
```
Pinia 没有 mutations,action 里写异步逻辑。比 Vuex 简洁很多,Vue 3 项目直接选它,不用犹豫。
## 7. 路由参数
跨页面传递首选路由参数或 query,刷新后还在。需要"可分享的链接"就必须走这条路,而不是存 Pinia。
## 怎么选
按这个顺序问自己:
1. 父子之间?→ props / emit
2. 表单封装?→ v-model
3. 要调子组件方法?→ 模板 ref + defineExpose
4. 跨层但同分支?→ provide / inject
5. 跨路由?→ Pinia
6. 跨页面且要可分享?→ 路由参数
**越往下越重,成本越高。** 能在第 1 层解决的,不要用到第 6 层。
## 最后
通信方式的选择,本质上是在选"数据流有多可预测"。props 单向可追,事件总线完全不可追。工程上宁可多写几行 props,也别图省事上全局总线——半年后接手代码的人会感谢你。