Vue3中组件间通信详解

Vue3中组件间通信详解

  • [一、 父子组件通信(最基础、最高频)](#一、 父子组件通信(最基础、最高频))
    • [1. Props:父传子](#1. Props:父传子)
    • [2. Emits:子传父](#2. Emits:子传父)
    • [3. v-model / defineModel:双向绑定](#3. v-model / defineModel:双向绑定)
    • [4. Ref + DefineExpose:父调用子方法](#4. Ref + DefineExpose:父调用子方法)
  • [二、 跨层级/祖孙组件通信](#二、 跨层级/祖孙组件通信)
    • [1. Provide / Inject](#1. Provide / Inject)
  • [三、 兄弟组件/任意组件通信](#三、 兄弟组件/任意组件通信)
    • [1. Mitt 事件总线](#1. Mitt 事件总线)
  • [四、 全局状态管理](#四、 全局状态管理)
  • [五、 其他特殊场景](#五、 其他特殊场景)
    • [1. Slots(插槽):内容分发与定制](#1. Slots(插槽):内容分发与定制)
      • [场景 A:默认插槽与具名插槽](#场景 A:默认插槽与具名插槽)
      • [场景 B:作用域插槽(Scoped Slots)](#场景 B:作用域插槽(Scoped Slots))
    • [2. attrs:属性透传(Attribute Inheritance)](#2. attrs:属性透传(Attribute Inheritance))
      • [场景 A:禁用默认继承并手动控制绑定位置](#场景 A:禁用默认继承并手动控制绑定位置)
      • [场景 B:多层组件透传(高阶组件)](#场景 B:多层组件透传(高阶组件))
  • 六、总结与选型建议

在 Vue3 开发中,组件通信是构建复杂应用的核心技能。根据组件之间的关系(父子、兄弟、跨层级、全局),Vue3 提供了多种高效且规范的通信方案。

一、 父子组件通信(最基础、最高频)

父子组件通信遵循单向数据流原则:父组件通过属性向下传递数据,子组件通过事件向上传递消息。

1. Props:父传子

这是最基础的通信方式,数据由父组件流向子组件,子组件只能读取,不能直接修改。

  • 父组件:通过属性绑定传递数据。
  • 子组件:使用 defineProps 接收数据。支持类型校验、默认值和必填项设置。
js 复制代码
<!-- 父组件 Father.vue -->
<template>
  <Child :msg="message" :count="10" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const message = ref('Hello Vue3')
</script>
js 复制代码
<!-- 子组件 Child.vue -->
<script setup>
// 接收 props,支持类型校验
const props = defineProps({
  msg: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})
// 使用 props.msg
</script>

2. Emits:子传父

当子组件需要通知父组件更新数据或执行操作时,使用自定义事件。

  • 子组件:使用 defineEmits 定义事件,并通过 emit 触发。
  • 父组件:监听子组件触发的事件并处理。
js 复制代码
<!-- 子组件 Child.vue -->
<template>
  <button @click="handleClick">发送数据给父组件</button>
</template>

<script setup>
const emit = defineEmits(['update-data'])

const handleClick = () => {
  emit('update-data', '来自子组件的数据')
}
</script>
js 复制代码
<!-- 父组件 Father.vue -->
<template>
  <Child @update-data="receiveData" />
</template>

<script setup>
const receiveData = (payload) => {
  console.log('收到:', payload)
}
</script>

3. v-model / defineModel:双向绑定

用于实现父子组件数据的双向同步。Vue 3.4+ 推荐使用 defineModel 宏简化写法。

  • 原理:本质上是 props + emit('update:xxx') 的语法糖。
  • 用法:父组件使用 v-model,子组件使用 defineModel
js 复制代码
<!-- 父组件 -->
<template>
  <Child v-model:title="pageTitle" />
</template>

<!-- 子组件 Child.vue (Vue 3.4+) -->
<script setup>
const title = defineModel('title')
// 直接修改 title.value 即可同步到父组件
</script>

4. Ref + DefineExpose:父调用子方法

当父组件需要直接调用子组件内部的方法或访问其状态时使用。注意:这会破坏组件封装性,应谨慎使用。

  • 子组件:使用 defineExpose 暴露需要被访问的方法或属性。
  • 父组件:通过 ref 获取子组件实例。
js 复制代码
<!-- 子组件 Child.vue -->
<script setup>
const sayHi = () => console.log('Hi!')
// 暴露方法
defineExpose({ sayHi })
</script>
js 复制代码
<!-- 父组件 Father.vue -->
<template>
  <Child ref="childRef" />
  <button @click="callChild">调用子组件方法</button>
</template>

<script setup>
import { ref } from 'vue'
const childRef = ref(null)

const callChild = () => {
  childRef.value?.sayHi()
}
</script>

二、 跨层级/祖孙组件通信

当组件嵌套过深(如 A -> B -> C -> D),使用 Props 逐层传递(Prop Drilling)非常繁琐,此时可采用以下方案:

1. Provide / Inject

祖先组件提供数据,后代组件无论嵌套多深都可以注入使用。适合主题配置、用户信息等深层共享数据。

  • 祖先组件:使用 provide 提供数据。
  • 后代组件:使用 inject 注入数据。
js 复制代码
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme-key', theme)
</script>
js 复制代码
<!-- 深层后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme-key')
</script>

三、 兄弟组件/任意组件通信

1. Mitt 事件总线

Vue3 移除了原生的 $bus,推荐使用轻量级第三方库 mitt 实现发布-订阅模式。适合非父子、非全局状态的简单通信。

  • 注意:需在组件卸载时手动移除监听,防止内存泄漏。
javascript 复制代码
// utils/bus.js
import mitt from 'mitt'
export const emitter = mitt()
js 复制代码
<!-- 组件 A (发送) -->
<script setup>
import { emitter } from '@/utils/bus'
const send = () => {
  emitter.emit('my-event', 'data')
}
</script>
js 复制代码
<!-- 组件 B (接收) -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { emitter } from '@/utils/bus'

const handler = (data) => console.log(data)

onMounted(() => {
  emitter.on('my-event', handler)
})

onUnmounted(() => {
  emitter.off('my-event', handler) // 务必清理
})
</script>

四、 全局状态管理

对于大型项目,涉及多个组件共享复杂状态(如登录态、购物车、权限等),建议使用状态管理库。

1.Pinia (推荐)

Pinia 是 Vue 官方推荐的状态管理库,替代了 Vuex。它去除了 Mutation,仅保留 State、Getter、Action,对 TypeScript 支持更好,且体积更小。

  • 特点:模块化设计,无需嵌套模块,调试方便。
  • 用法:创建 Store,在组件中直接调用 store 的状态和方法。
javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const name = ref('Admin')
  function setName(newName) {
    name.value = newName
  }
  return { name, setName }
})
js 复制代码
<!-- 任意组件 -->
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 直接使用 userStore.name 或 userStore.setName()
</script>

五、 其他特殊场景

1. Slots(插槽):内容分发与定制

插槽允许父组件向子组件传递模板结构,实现"结构由子组件定,内容由父组件定"的解耦。

场景 A:默认插槽与具名插槽

适用于布局组件(如卡片、弹窗),其中部分区域固定,部分区域需自定义。

子组件 Card.vue

js 复制代码
<template>
  <div class="card">
    <!-- 具名插槽:头部 -->
    <header class="card-header">
      <slot name="header">默认标题</slot>
    </header>
    
    <!-- 默认插槽:主体内容 -->
    <main class="card-body">
      <slot>默认主体内容</slot>
    </main>
    
    <!-- 具名插槽:底部操作区 -->
    <footer class="card-footer">
      <slot name="footer">
        <button>默认按钮</button>
      </slot>
    </footer>
  </div>
</template>

<style scoped>
.card { border: 1px solid ddd; padding: 10px; }
.card-header { font-weight: bold; margin-bottom: 10px; }
.card-footer { margin-top: 10px; text-align: right; }
</style>

父组件 Parent.vue使用

js 复制代码
<template>
  <Card>
    <!-- 1. 具名插槽写法:v-slot:name 或简写 name -->
    <template header>
      <h2>自定义标题</h2>
    </template>

    <!-- 2. 默认插槽:直接写内容,无需 template 包裹(也可用 default) -->
    <p>这是父组件传入的动态正文内容。</p>

    <!-- 3. 具名插槽:底部 -->
    <template footer>
      <button @click="save">保存</button>
      <button @click="cancel">取消</button>
    </template>
  </Card>
</template>

<script setup>
import Card from './Card.vue'
const save = () => console.log('保存')
const cancel = () => console.log('取消')
</script>

场景 B:作用域插槽(Scoped Slots)

适用于列表、表格等组件,子组件提供数据,父组件决定如何渲染这些数据。

子组件 UserList.vue

js 复制代码
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- 
        通过 :user="user"将数据暴露给父组件 
        父组件接收时可以使用任意变量名,如 slotProps
      -->
      <slot :user="user" :index="user.id">
        <!-- 默认渲染方式 -->
        {{ user.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'
const users = ref([
  { id: 1, name: 'Alice', role: 'Admin' },
  { id: 2, name: 'Bob', role: 'User' }
])
</script>

父组件 Parent.vue 使用

js 复制代码
<template>
  <UserList>
    <!-- 
      v-slot:default="slotProps" 
      slotProps 包含子组件暴露的所有属性: { user, index }
    -->
    <template default="{ user, index }">
      <div class="custom-item">
        <span>{{ index }}.</span>
        <strong>{{ user.name }}</strong> 
        <span v-if="user.role === 'Admin'" class="badge">管理员</span>
      </div>
    </template>
  </UserList>
</template>

<script setup>
import UserList from './UserList.vue'
</script>

<style scoped>
.badge { background: red; color: white; padding: 2px 5px; font-size: 12px; }
</style>

2. $attrs:属性透传(Attribute Inheritance)

$attrs 包含父组件传递给子组件但未被声明为 props 且未被监听为 emits 的所有属性(包括 class, style, 事件监听器等)。常用于高阶组件或封装第三方库组件。

场景 A:禁用默认继承并手动控制绑定位置

默认情况下,未声明的属性会自动绑定到组件的根元素。如果希望绑定到内部特定元素(如 input),需禁用默认继承。

子组件 CustomInput.vue

js 复制代码
<template>
  <div class="input-wrapper">
    <label>标签:</label>
    <!-- 
      1. defineOptions({ inheritAttrs: false }) 禁用了自动绑定到根 div
      2. v-bind="$attrs" 手动将属性绑定到 input 上
      这样父组件传的 placeholder, @focus, class 等都会生效在 input 上
    -->
    <input v-bind="$attrs" />
  </div>
</template>

<script setup>
// Vue 3.3+ 推荐写法
defineOptions({
  inheritAttrs: false
})

// 注意:这里没有定义任何 props,所以所有传入属性都会进入 $attrs
</script>

父组件 Parent.vue 使用

js 复制代码
<template>
  <!-- 
    placeholder, @focus, class 均未在子组件 props 中声明
    它们会通过 $attrs 透传到内部的 input 元素上
  -->
  <CustomInput 
    placeholder="请输入内容" 
    @focus="onFocus" 
    class="custom-style" 
  />
</template>

<script setup>
import CustomInput from './CustomInput.vue'
const onFocus = () => console.log('输入框获得焦点')
</script>

场景 B:多层组件透传(高阶组件)

当组件只是作为中间层,需要将属性继续传递给更深层的子组件时。

中间组件 Wrapper.vue

js 复制代码
<template>
  <div class="wrapper-border">
    <!-- 将接收到的所有未声明属性透传给 BaseButton -->
    <BaseButton v-bind="$attrs">
      <!-- 如果需要透传插槽,也可以显式写出 -->
      <slot></slot>
    </BaseButton>
  </div>
</template>

<script setup>
import BaseButton from './BaseButton.vue'
// Wrapper 不声明任何 props,所有属性直接透传
</script>

底层组件 BaseButton.vue

js 复制代码
<template>
  <button v-bind="$attrs">
    <slot></slot>
  </button>
</template>

<script setup>
// 同样不声明 props,确保属性最终绑定到 button 元素
defineOptions({ inheritAttrs: false })
</script>

父组件使用

js 复制代码
<template>
  <!-- type, disabled, @click 等属性会穿过 Wrapper,最终绑定到 BaseButton 的 button 标签上 -->
  <Wrapper type="submit" disabled @click="handleSubmit">
    提交
  </Wrapper>
</template>

六、总结与选型建议

场景 推荐方案 备注
父传子 props 单向数据流,最基础
子传父 emits 保持单向流,解耦
父子双向同步 v-model / defineModel 表单、输入框常用
父调用子方法 ref + defineExpose 慎用,破坏封装
跨多层级传递 provide / inject 避免 Prop Drilling
兄弟/任意组件 Mitt 事件总线 轻量级,需手动清理监听
大型全局状态 Pinia 官方推荐,替代 Vuex
内容布局定制 Slots 特别是作用域插槽

在实际开发中,应优先使用 Props/Emits 和 Pinia,仅在特定场景下使用 Provide/Inject 或 Mitt,以保持代码的可维护性和清晰的数据流向。

相关推荐
程序员黑豆2 小时前
从零开始:创建第一个鸿蒙应用程序
前端·harmonyos
只与明月听2 小时前
LangChain 学习-掌握LangChain Core API
前端·人工智能·后端
Listen·Rain2 小时前
Vue3中customRef详解
前端·javascript·vue.js
濮水大叔2 小时前
Hooks 之后,为什么前端仍需要“包裹”能力?
vue.js·react.js·前端框架
sugar__salt2 小时前
Document 切割:RAG 知识库数据预处理实战
javascript·langchain·embedding·js·rag·cheerio
小粉粉hhh2 小时前
React(三)——Redux
前端·javascript·react.js
山东点狮信息科技有限公司3 小时前
企业级开源OA系统推荐
vue.js·spring boot·性能优化·系统架构·开源
易知微EasyV数据可视化3 小时前
定制化3D组件开发:打造差异化孪生体验
前端·人工智能·经验分享·数字孪生
多加点辣也没关系3 小时前
JavaScript|第24章:事件循环与并发模型
开发语言·javascript·ecmascript