Code前端首页关于Code前端联系我们

Vue3 跨组件通信有哪些实用方法?

terry 23小时前 阅读数 16 #Vue
文章标签 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包的),后代组件拿到的是“快照”,数据变了也不更新!所以一定要用refreactive包起来,像上面例子里的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不用写mutationsactions直接改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事件,否则多次创建组件会重复监听,导致逻辑错乱。
  • Teleportto指定的目标元素必须存在(比如body肯定在,但自己写的<div id="target"></div>要确保渲染了再传),否则会报错。

现在项目里遇到跨组件通信,按场景选方法,再也不用抓瞎啦~要是还没搞懂,评论区留言,咱们再唠细节!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门