Vue组件通信方式汇总
一句话概括
Vue 3 的组件通信不止 props + emit——从父子到全局,有超过十种方式,选型的关键是按耦合度匹配场景:亲近用 props/emit,跨层级用 provide/inject,全局用 Pinia,模板定制用插槽。
核心知识点
1. props + emit(最基础的父子通信)
1
2
3
4
5
6
7
8
9
10
<!-- 父组件 -->
<Child :name="userName" @update="handleUpdate" />
<!-- 子组件 -->
<script setup>
const props = defineProps<{ name: string }>()
const emit = defineEmits<{ update: [value: string] }>()
emit('update', '新值')
</script>
defineProps 和 defineEmits 是编译宏,无需 import。TS 泛型版本(Vue 3.3+)让类型推导零成本。
2. v-model / defineModel(双向绑定)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 父组件 -->
<CustomInput v-model="text" />
<!-- 子组件 (Vue 3.4+) -->
<script setup>
const model = defineModel()
// model.value 自动同步 parent 值
</script>
<!-- 子组件 (Vue 3.3 及以下) -->
<script setup>
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
</script>
defineModel(3.4+)是双向绑定的现代写法,不需要手动 emit。支持多 v-model:v-model:title。
3. provide / inject(深层传递,跨层级利器)
1
2
3
4
5
6
7
8
// 祖先组件
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme) // key 建议用 Symbol 避免冲突
// 任意后代组件(不用经过中间层!)
import { inject } from 'vue'
const theme = inject('theme')
provide 的值如果是 ref/reactive,后代拿到的就是响应式引用,祖先改了后代自动更新。这是替代 Vue 2 的 $parent/$children 链的正道。
4. 插槽(Slot)—— 模板级别的通信
1
2
3
4
5
6
7
8
9
<!-- 父组件给子组件传"模板片段" -->
<DataTable :data="users">
<template #cell="{ value }">
<span class="highlight">{{ value }}</span>
</template>
</DataTable>
<!-- 子组件 -->
<slot name="cell" :value="item" />
作用域插槽的 value 是子组件暴露给父组件的——数据从子流向父再由父渲染回子,是 Vue 特有的「控制反转」模式。
5. ref + defineExpose(命令式调用子组件)
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 父组件 -->
<Child ref="childRef" />
<script setup>
const childRef = ref()
childRef.value.focusInput()
</script>
<!-- 子组件 -->
<script setup>
const inputEl = ref()
defineExpose({ focusInput: () => inputEl.value.focus() })
</script>
紧耦合,只在必要时用(如 focus 表单、触发动画)。能走 props/emit 绝不走 ref。
6. Pinia(全局状态,松耦合首选)
1
2
3
4
const store = useUserStore()
// 任意组件里直接读/写,零耦合
store.login()
store.name // 响应式
相比 mitt 事件总线,Pinia 有类型推导 + Devtools 调试 + 持久化插件,是全局共享状态的标准答案。
7. 通信方式速查表
| 方式 | 场景 | 耦合度 | 方向 |
|---|---|---|---|
| props + emit | 父子直接交互 | 强 | 单向+回调 |
| v-model / defineModel | 表单控件 | 中 | 双向 |
| provide/inject | 祖先→任意后代 | 弱 | 单向 |
| slot | 模板定制 | 中 | 父↔子 |
| ref + expose | 命令式调用 | 强 | 父→子 |
| Pinia | 全局共享 | 无 | 集中 |
| mitt | 非层级组件 | 松散 | 任意 |
「其实你每天都在用」
- 表单组件封装:
<SearchInput v-model="keyword">,子组件改值父组件自动同步。 - 全局主题/语言:
provide('locale', locale)往根组件一放,所有页面inject就能读到。 - 表格封装:
<Table :columns="cols" :data="rows"><template #action>...</template></Table>,列配置和操作列通过 slot 灵活定制。 - 跨组件触发刷新:列表页删数据后通知详情页关闭,Pinia 里设个
refreshFlag,详情页watch监听。 - 弹窗封装:
<Modal v-model:visible="show">...内容的slot...</Modal>,双向绑定控制开关,slot 控制内容。
常见误解(FAQ)
❌ 误区:provide/inject 不是响应式的。 如果你 provide 的是一个 ref 或 reactive 对象,那它就是响应式的。只有 provide 原始值(字符串、数字)才是不响应的。最佳实践:provide 永远包一层 ref。
❌ 误区:兄弟组件通信必须用 Pinia。 如果共同父组件本来就存在,把状态提升到父组件用 props 下传最简单。Pinia 适合真正的跨层级/跨页面共享。
❌ 误区:Vue 3 不支持事件总线了。 $on/$off/$emit 实例方法确实移除了,但可以用 mitt 库(Vue 官方推荐)。不过事件总线的无类型、不可追踪特性让它只适合极少数场景,大部分已被 Pinia 替代。
❌ 误区:defineExpose 暴露的方法可以随意用。 defineExpose 是设计给父组件调用子组件的「逃生口」,不应该成为常规通信方式。像 focus()、reset() 这种无副作用的命令式操作可以接受,但不应通过它传递业务状态。
一句话总结
选通信方式跟选交通方式一样——隔壁办公室走过去(props/emit),跨楼层坐电梯(provide/inject),跨城市坐高铁(Pinia),别为了见隔壁同事打飞的。