Vue3 跨组件通信有哪些实用方法?
做Vue3项目时,跨组件传值总卡壳?父子、祖孙、兄弟组件间数据咋流畅互通?别慌,这篇把常用跨组件通信方法拆明白,从基础到进阶,结合场景讲透怎么选、怎么用~
父子组件:Props + Emits 基础流
最常见的父子通信,就靠Props(父传子)和Emits(子传父)这对「老搭档」。
父传子(Props)怎么玩?
假设做个商品卡片组件,父组件要把商品名称、价格传给子组件,父组件里这样写:
<template>
<ProductCard :title="productTitle" :price="productPrice" />
</template>
<script setup>
import ProductCard from './ProductCard.vue'
import { ref } from 'vue'
const productTitle = ref('Vue3实战指南')
const productPrice = ref(99)
</script>
子组件ProductCard.vue里,先声明接收的Props,再用:
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>价格:{{ price }} 元</p>
</div>
</template>
<script setup>
const props = defineProps({ String,
price: Number
})
</script>
这样父组件的数据就「流」到子组件啦~
子传父(Emits)咋操作?
比如子组件有个“加入购物车”按钮,点击后要把选的商品信息传给父组件,子组件里定义触发的事件:
<template>
<button @click="addToCart">加入购物车</button>
</template>
<script setup>
const emit = defineEmits(['add-cart'])
const addToCart = () => {
// 假设商品信息是固定的,实际可动态获取
emit('add-cart', { title: 'Vue3实战指南', price: 99 })
}
</script>
父组件里监听这个事件,处理逻辑:
<template>
<ProductCard @add-cart="handleAddCart" />
</template>
<script setup>
const handleAddCart = (product) => {
console.log('父组件收到:', product) // 就能拿到子组件传的数据
}
</script>
这种方式简单直接,适合层级浅、数据流向明确的父子场景,缺点是层级深时逐层传Props太繁琐,得换其他方法~
祖孙组件:Provide + Inject 穿透流
要是组件嵌套了好几层(比如父→子→孙→曾孙),总不能让父组件的数据逐层传给曾孙吧?这时候Provide + Inject就像「传送隧道」,跳过中间层直接传。
基本用法咋整?
祖先组件(比如App.vue)里用provide提供数据:
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light') // 响应式数据,用ref包起来
provide('theme-key', theme) // 第一个参数是key,第二个是要传的数据
</script>
后代组件(不管嵌套多深,比如GreatGrandChild.vue)里用inject接收:
<template>
<div :class="theme">当前主题:{{ theme }}</div>
</template>
<script setup>
import { inject } from 'vue'
const theme = inject('theme-key') // 通过key拿到数据
</script>
这样不管中间隔多少层,后代组件都能拿到祖先给的数据~
注意!响应式要“保鲜”
如果provide的是普通变量(不是ref/reactive包的),后代组件拿到的是“快照”,数据变了也不更新!所以一定要用ref或reactive包起来,像上面例子里的theme用了ref,祖先组件改theme.value = 'dark',后代组件能自动更新~
适合场景:多层嵌套的全局配置(比如主题、用户信息),不用每层都写Props,减少代码冗余~
任意组件:Pinia/Vuex 状态管理流
要是很多组件都要共享复杂数据(比如购物车商品、用户登录状态),用Props/Provide来回传太乱了,这时候得请「状态管理库」出马,Vue3推荐用Pinia(Vuex的升级版,更轻量好用)。
Pinia咋用?
先定义一个Store(比如cartStore.js):
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] // 购物车商品列表
}),
actions: {
addItem(product) {
this.items.push(product)
},
removeItem(id) {
this.items = this.items.filter(item => item.id !== id)
}
},
getters: {
totalPrice() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
}
})
然后在组件里用:
<template>
<button @click="addBookToCart">加本书到购物车</button>
<p>购物车总价:{{ cartStore.totalPrice }}</p>
</template>
<script setup>
import { useCartStore } from './cartStore.js'
const cartStore = useCartStore() // 拿到store实例
const addBookToCart = () => {
cartStore.addItem({ id: 1, title: 'Vue3实战', price: 89 })
}
</script>
不管哪个组件,只要调用useCartStore(),就能拿到同一套数据,修改后所有组件自动更新~
和Vuex比有啥优势?
Pinia不用写mutations(actions直接改state),语法更简洁;支持Composition API写法;体积更小,Vue3项目优先选它~
适合场景:多组件共享的复杂状态(购物车、全局弹窗、用户权限),让数据管理更集中,避免“Props drilling”(逐层传Props的麻烦)~
兄弟/无关联组件:事件总线(mitt库)
如果两个组件没啥血缘关系(比如导航栏和侧边栏是兄弟,或者完全不相关的组件),咋传数据?Vue2里能用this.$bus,但Vue3把实例上的事件API删了,得用第三方库mitt。
mitt咋实现事件总线?
先装依赖:npm i mitt
然后新建bus.js:
import mitt from 'mitt' export const bus = mitt() // 创建事件总线实例
组件A(比如Navbar.vue)触发事件:
<template>
<button @click="notifySidebar">切换侧边栏</button>
</template>
<script setup>
import { bus } from './bus.js'
const notifySidebar = () => {
bus.emit('toggle-sidebar', true) // 传事件名和数据
}
</script>
组件B(比如Sidebar.vue)监听事件:
<template>
<div v-show="isOpen">侧边栏内容</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { bus } from './bus.js'
const isOpen = ref(false)
onMounted(() => {
bus.on('toggle-sidebar', (val) => { // 监听事件
isOpen.value = val
})
})
onUnmounted(() => {
bus.off('toggle-sidebar') // 组件销毁前取消监听,防止内存泄漏
})
</script>
这样两个无关组件就能通过事件总线通信啦~
注意点:记得销毁事件
如果组件销毁后不off,下次再创建可能重复监听,导致逻辑混乱,所以onUnmounted里一定要写off~
适合场景:无层级关系的组件通信(兄弟、跨页面组件),但别滥用,复杂场景优先用Pinia~
跨层级DOM通信:Teleport 传送门
Teleport不是「数据通信」,是DOM结构的跨层级渲染,但结合数据控制(比如弹窗显示隐藏),也能解决跨组件的交互问题。
啥场景用?
比如有个深嵌套的子组件里要弹弹窗,但父组件的样式有overflow: hidden,弹窗被截断了,这时候用Teleport把弹窗的DOM“传送”到body下,就能正常显示。
代码咋写?
子组件Popup.vue:
<template>
<Teleport to="body"> <!-- 把内容渲染到body里 -->
<div class="popup-mask" v-show="isShow">
<div class="popup-content">
<p>我是弹窗内容</p>
<button @click="closePopup">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
isShow: Boolean
})
const emit = defineEmits(['close'])
const closePopup = () => {
emit('close')
}
</script>
父组件里控制显示:
<template>
<button @click="showPopup = true">打开弹窗</button>
<Popup :is-show="showPopup" @close="showPopup = false" />
</template>
<script setup>
import Popup from './Popup.vue'
import { ref } from 'vue'
const showPopup = ref(false)
</script>
这样弹窗的DOM被渲染到body下,样式层级问题解决,同时父组件通过Props控制显示,子组件通过Emits通知关闭,实现跨层级交互~
适合场景:全局弹窗、提示框、加载层等需要脱离当前组件DOM层级的元素,数据通信还是靠Props/Emits配合~
实战场景选哪个?避坑指南
不同场景对应不同方法,选错了代码又乱又难维护,记住这些「选法」和「坑」:
选法速查表
- 父子组件,数据流向明确 → Props + Emits
- 多层嵌套的祖孙组件,传全局配置 → Provide + Inject(记得包
ref/reactive保持响应式) - 多组件共享复杂状态(购物车、用户信息) → Pinia(Vue3优先用,别纠结Vuex)
- 无关联/兄弟组件,简单交互 → mitt事件总线(记得销毁事件)
- 需要跨DOM层级的弹窗/提示 → Teleport(配合
Props/Emits控制显示)
避坑细节
- Props:要写类型验证(
defineProps({ name: String })),防止传错类型;非必传要加default。 - Emits:事件名要和父组件
@xxx匹配,最好用常量管理事件名(比如const EMIT_ADD = 'add-cart'),避免拼写错。 - Provide/Inject:如果传的是对象/数组,直接改内部属性能响应,但替换整个对象要用
ref.value = 新值;简单类型(字符串、数字)必须包ref才会响应。 - Pinia:别把所有数据都丢进Store,只存跨组件共享的状态,组件内部私有数据还是放组件里。
- mitt:组件销毁时一定要
off事件,否则多次创建组件会重复监听,导致逻辑错乱。 - Teleport:
to指定的目标元素必须存在(比如body肯定在,但自己写的<div id="target"></div>要确保渲染了再传),否则会报错。
现在项目里遇到跨组件通信,按场景选方法,再也不用抓瞎啦~要是还没搞懂,评论区留言,咱们再唠细节!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。