文章

Vue组件通信方式汇总

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>

definePropsdefineEmits 是编译宏,无需 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非层级组件松散任意

「其实你每天都在用」

  1. 表单组件封装<SearchInput v-model="keyword">,子组件改值父组件自动同步。
  2. 全局主题/语言provide('locale', locale) 往根组件一放,所有页面 inject 就能读到。
  3. 表格封装<Table :columns="cols" :data="rows"><template #action>...</template></Table>,列配置和操作列通过 slot 灵活定制。
  4. 跨组件触发刷新:列表页删数据后通知详情页关闭,Pinia 里设个 refreshFlag,详情页 watch 监听。
  5. 弹窗封装<Modal v-model:visible="show">...内容的slot...</Modal>,双向绑定控制开关,slot 控制内容。

常见误解(FAQ)

❌ 误区:provide/inject 不是响应式的。 如果你 provide 的是一个 refreactive 对象,那它就是响应式的。只有 provide 原始值(字符串、数字)才是不响应的。最佳实践:provide 永远包一层 ref

❌ 误区:兄弟组件通信必须用 Pinia。 如果共同父组件本来就存在,把状态提升到父组件用 props 下传最简单。Pinia 适合真正的跨层级/跨页面共享。

❌ 误区:Vue 3 不支持事件总线了。 $on/$off/$emit 实例方法确实移除了,但可以用 mitt 库(Vue 官方推荐)。不过事件总线的无类型、不可追踪特性让它只适合极少数场景,大部分已被 Pinia 替代。

❌ 误区:defineExpose 暴露的方法可以随意用。 defineExpose 是设计给父组件调用子组件的「逃生口」,不应该成为常规通信方式。像 focus()reset() 这种无副作用的命令式操作可以接受,但不应通过它传递业务状态。

一句话总结

选通信方式跟选交通方式一样——隔壁办公室走过去(props/emit),跨楼层坐电梯(provide/inject),跨城市坐高铁(Pinia),别为了见隔壁同事打飞的。

本文由作者按照 CC BY 4.0 进行授权