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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。