Vue 中组件通信有哪些方式?该用哪个?
Vue 中组件通信有哪些方式?该用哪个?
一句话总结:父子用 props/emit,跨级用 provide/inject,兄弟用状态提升或 EventBus,全局用 Pinia。按关系选方案,别上来就 Vuex/Pinia!
正文目录
- Vue 组件通信全景图
- 5 大高频场景 & 最佳实践
- 万能兜底:按关系选方案
- 预防 checklist(不再踩坑)
- 一句话总结
一、Vue 组件通信全景图
Vue 组件通信的本质就是:数据怎么从 A 传到 B,B 怎么通知 A。
按组件关系分类:
| 关系 | 推荐方案 | 一句话 |
|---|---|---|
| 父 → 子 | props | 父亲给孩子东西 |
| 子 → 父 | emit | 孩子喊一声通知父亲 |
| 父 → 子(直接操作) | ref + expose | 父亲直接伸手 |
| 祖先 → 后代 | provide / inject | 祖宗传家宝,后代直接拿 |
| 兄弟之间 | 状态提升 / EventBus | 找共同的父亲中转 |
| 任意组件 | Pinia | 全局仓库,谁都能取 |
常见误区:上来就 Pinia,简单父子通信也套全局状态,代码复杂度暴增。
二、5 大高频场景 & 最佳实践
① 父子通信:props 传了对象,子组件直接改
<!-- ❌ 子组件直接修改 props 对象的属性 --> <script setup> const props = defineProps(['user']); const changeName = () => { props.user.name = 'new'; // ❌ 虽然能改,但数据流不清晰 }; </script>修复:子组件 emit,父组件改
<!-- ✅ 子组件 emit 事件,父组件修改数据 --> <script setup> const props = defineProps(['user']); const emit = defineEmits(['update']); const changeName = () => { emit('update', { ...props.user, name: 'new' }); // ✅ emit 新对象 }; </script>② 跨级通信:层层透传 props(prop drilling)
<!-- ❌ 中间组件不需要这个 prop,只是透传 --> <!-- Grandfather.vue --> <Template :theme="theme" /> <!-- Father.vue (不需要 theme,但必须接) --> <script setup> defineProps(['theme']); // ❌ 纯透传 </script> <template> <Child :theme="theme" /> </template> <!-- Child.vue --> <script setup> defineProps(['theme']); </script>修复:用 provide/inject
<!-- ✅ 祖先组件 provide --> <!-- Grandfather.vue --> <script setup> import { ref, provide } from 'vue'; const theme = ref('dark'); provide('theme', theme); // ✅ 注入 </script> <template> <Father /> </template><!-- ✅ 后代组件 inject,中间层完全不需要知道 --> <!-- Child.vue --> <script setup> import { inject } from 'vue'; const theme = inject('theme'); // ✅ 直接拿 </script>③ 兄弟通信:用 EventBus 但忘记销毁
// ❌ EventBus 方案,组件销毁时没移除监听 → 内存泄漏importmittfrom'mitt';constbus=mitt();// BrotherA.vuebus.emit('refresh',data);// BrotherB.vuebus.on('refresh',handler);// ❌ 没有 bus.off,切走后还在监听修复方案一:onUnmounted 时移除监听
// ✅ 组件销毁时移除import{onUnmounted}from'vue';bus.on('refresh',handler);onUnmounted(()=>{bus.off('refresh',handler);// ✅ 清理});修复方案二(推荐):状态提升到共同父组件
<!-- ✅ 共同父组件管理状态,用 props/emit 传给两个子组件 --> <script setup> import { ref } from 'vue'; const sharedData = ref(null); const updateData = (val) => { sharedData.value = val; }; </script> <template> <BrotherA :data="sharedData" @update="updateData" /> <BrotherB :data="sharedData" @update="updateData" /> </template>④ 父组件直接调子组件方法:用 ref 但子组件没 expose
<!-- ❌ 父组件拿到 ref,但调不到子组件方法 --> <script setup> import { ref } from 'vue'; const childRef = ref(null); const callChild = () => { childRef.value?.doSomething(); // ❌ 子组件没 expose,拿到的是 undefined }; </script> <template> <Child ref="childRef" /> </template><!-- 子组件 --> <script setup> const doSomething = () => { /* ... */ }; // ❌ 没有 defineExpose,外部访问不到 </script>修复:子组件用 defineExpose
<!-- ✅ 子组件暴露方法 --> <script setup> const doSomething = () => { /* ... */ }; const reset = () => { /* ... */ }; defineExpose({ doSomething, reset }); // ✅ 主动暴露 </script>⑤ 全局状态:该用 Pinia 却用了 provide/inject
<!-- ❌ 全局用户信息用 provide/inject,刷新丢失、无法持久化 --> <!-- App.vue --> <script setup> import { ref, provide } from 'vue'; const user = ref(null); provide('user', user); // ❌ 全局状态不该用 provide,没法跨路由管理 </script>修复:用 Pinia
// ✅ stores/user.jsimport{defineStore}from'pinia';import{ref}from'vue';exportconstuseUserStore=defineStore('user',()=>{constuser=ref(null);constsetUser=(val)=>{user.value=val;};constlogout=()=>{user.value=null;};return{user,setUser,logout};});<!-- ✅ 任意组件直接用 --> <script setup> import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); // userStore.user, userStore.setUser(), userStore.logout() </script>三、万能兜底:按关系选方案
| 组件关系 | 首选方案 | 备选方案 |
|---|---|---|
| 父 → 子 | props | ref + expose |
| 子 → 父 | emit | — |
| 祖先 → 后代 | provide/inject | Pinia |
| 兄弟 | 状态提升 | EventBus / Pinia |
| 任意(全局) | Pinia | — |
| 临时事件总线 | mitt | Pinia(如果状态多了就迁移) |
决策流程图:
需要通信的组件关系是什么? ├── 父子 → props + emit ├── 祖先后代 → provide/inject ├── 兄弟 → 状态提升到共同父级 │ └── 父级太远?→ Pinia └── 任意组件 → Pinia └── 只是临时事件?→ mitt EventBus(记得 off)四、预防 checklist
- 父子通信优先props + emit,不要用 ref 直接操作子组件(除非表单聚焦等场景)
- 跨级传 prop超过 2 层就换provide/inject
- 兄弟通信先考虑状态提升,层级太深再上Pinia
- 用 EventBus必须在 onUnmounted 里 off,否则内存泄漏
- 父组件 ref 调子组件方法,子组件必须 defineExpose
- 全局状态(用户信息、主题、权限)用 Pinia,不要用 provide/inject
- provide 传响应式数据时传 ref/computed,别传普通值
- inject 时给默认值:
inject('key', defaultValue),防止找不到 provider
五、一句话总结
「组件通信按关系选方案」:父子 props/emit,跨级 provide/inject,兄弟状态提升,全局 Pinia。别简单通信套全局状态,也别复杂状态硬透传 props。选对工具,数据流才清晰!
最后问候亲爱的朋友们,并邀请你们阅读我的全新著作
📚 《Vue.js 3企业级项目开发实战(微课视频版》