一、props / emits:最基础,也最容易被用坏的"父子直连"🔗
1️⃣ 本质一句话
- props:父 → 子(数据下发)
- emits:子 → 父(事件通知)
这是 Vue 官方最推荐、最安全、最清晰的通信方式,没有之一。
2️⃣ 标准用法(别省这两行声明)
vue
<!-- Child.vue -->
<script setup>
const props = defineProps({
count: Number
})
const emit = defineEmits(['update'])
</script>
<template>
<button @click="emit('update', props.count + 1)">
+1
</button>
</template>
vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
<template>
<Child :count="count" @update="count = $event" />
</template>
3️⃣ 什么时候必须用 props / emits?
- 父子组件关系明确
- 数据流需要可追踪、可调试
- 想要单向数据流(父是"老板",子只汇报)
🚨 常见翻车现场
- ❌ 子组件直接改 props
- ❌ props 当全局状态用
- ❌ emits 名字随便起(
change、handle、xxxEvent满天飞)
👉 口诀:
父给数据,子不碰;
子要改,喊一声(emit)。
二、provide / inject:跨层通信,但千万别"滥权"🧬
1️⃣ 本质一句话
祖先组件提供(provide),任意后代组件注入(inject)
它解决的是:
👉 中间层不关心,但你又不想层层 props 传递 的问题。
2️⃣ 基本用法
js
// Parent.vue
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
js
// DeepChild.vue
import { inject } from 'vue'
const theme = inject('theme')
3️⃣ 使用场景(很重要)
✅ 组件库/基础设施类数据
- 主题(theme)
- 国际化(i18n)
- 表单上下文(Form / FormItem)
- 当前用户只读信息
🚨 provide / inject 的三大雷区
1️⃣ inject 的数据来源不清晰 (你不知道它从哪来)
2️⃣ 默认不是响应式 (要传 ref / reactive 才行)
3️⃣ 被当成"低配版状态管理"
👉 一句狠话:
provide/inject 是"上下文",不是"仓库"。
三、ref / expose:能用,但要克制的"命令式通信"⚠️
1️⃣ 本质一句话
父组件通过 ref,直接调用子组件方法或访问实例
2️⃣ 标准写法(Vue3 必须 expose)
vue
<!-- Child.vue -->
<script setup>
function reset() {
console.log('reset')
}
defineExpose({ reset })
</script>
vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
function handleClick() {
childRef.value.reset()
}
</script>
<template>
<Child ref="childRef" />
<button @click="handleClick">重置子组件</button>
</template>
3️⃣ 适合的场景
✅ 表单校验 / 重置
✅ 弹窗打开 / 关闭
✅ 视频播放控制
✅ 聚焦输入框
🚨 不适合的场景
❌ 数据同步
❌ 状态驱动 UI
❌ 多层组件依赖
👉 记住这句话:
ref/expose 是"遥控器",不是"数据通道"。
四、事件总线:不是不能用,而是"别乱用"📡
1️⃣ 本质一句话
一个全局的事件发布/订阅中心
js
// bus.js
import mitt from 'mitt'
export const bus = mitt()
js
// A.vue
bus.emit('login', user)
js
// B.vue
bus.on('login', (user) => {})
2️⃣ 什么时候还能接受?
✅ 临时工具型功能
- Toast / Modal / 全局提示
- 埋点事件
- 非核心业务
🚨 为什么不推荐?
- 事件来源不可追踪
- 容易忘记 off,内存泄漏
- 事件名冲突
- 项目一大,灾难级调试体验
👉 真实项目经验:
事件总线一多,迟早会有人问:
"这个事件是谁发的?"
然后,全员沉默 😶
五、状态管理方案对比:什么时候该"上仓库"?📦
1️⃣ 常见方案一览
| 方案 | 适合场景 |
|---|---|
| props / emits | 父子通信 |
| provide / inject | 跨层上下文 |
| ref / expose | 命令式控制 |
| 事件总线 | 临时、工具级 |
| Pinia / Vuex | 全局业务状态 |
2️⃣ Pinia 适合什么?
✅ 用户信息
✅ 登录态
✅ 权限
✅ 多页面共享状态
✅ 跨模块通信
js
// userStore.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null
}),
actions: {
setUser(u) {
this.user = u
}
}
})
js
// 任意组件
const userStore = useUserStore()
userStore.setUser({ name: 'Tom' })
🚨 别什么都丢进状态管理
- 表单临时状态 ❌
- UI 展示态 ❌
- 单组件内部状态 ❌
👉 状态管理是"仓库",不是"垃圾桶"。
六、终极选型指南(直接抄)🧠
✅ 优先级排序(从强烈推荐到谨慎使用)
1️⃣ props / emits (80% 场景)
2️⃣ provide / inject (上下文)
3️⃣ ref / expose (命令式)
4️⃣ 状态管理(Pinia) (全局业务)
5️⃣ 事件总线(能不用就不用)
✅ 一句话决策法
- 父子关系?👉 props / emits
- 跨层共享但非业务核心?👉 provide / inject
- 需要直接调用方法?👉 ref / expose
- 多页面、多模块共享?👉 Pinia
- 想偷懒?👉 先冷静一下 😅
结尾反问(送你当文章收尾钩子😏)
组件通信方式从来不是"越高级越好",而是"越清晰越值钱 "。
所以我想反问你一句:
你现在用的通信方式,是在"表达关系",还是在"掩盖混乱"?