【Vue进阶学习笔记】组件通信专题精讲

目录

  • 前言
  • [props 父传子](#props 父传子)
  • [自定义事件 emit 子传父](#自定义事件 emit 子传父)
  • [Event Bus 兄弟/跨层通信](#Event Bus 兄弟/跨层通信)
  • [v-model 父子双向绑定](#v-model 父子双向绑定)
  • [\attrs / listeners 属性和事件透传](#$attrs / $listeners 属性和事件透传)
  • [ref / parent / children 获取组件实例](#ref / parent / children 获取组件实例)
      • 原理说明
      • 使用场景
      • 代码示例
        • [父组件 RefChildrenParentTest.vue](#父组件 RefChildrenParentTest.vue)
        • [子组件 Son.vue](#子组件 Son.vue)
        • [子组件 Daughter.vue](#子组件 Daughter.vue)
  • [provide / inject 跨级通信](#provide / inject 跨级通信)
      • 原理说明
      • 使用场景
      • 代码示例
        • [祖先组件 ProvideInjectTest.vue](#祖先组件 ProvideInjectTest.vue)
        • [子组件 Child.vue](#子组件 Child.vue)
        • [孙子组件 GrandChild.vue](#孙子组件 GrandChild.vue)
  • [Pinia 全局状态管理](#Pinia 全局状态管理)
      • 原理说明
      • 使用场景
      • 代码示例
        • [1. 安装 Pinia](#1. 安装 Pinia)
        • [2. 创建并注册 Pinia](#2. 创建并注册 Pinia)
        • [3. 定义 Store(全局计数器,js 版)](#3. 定义 Store(全局计数器,js 版))
        • [4. 父组件下两个子组件共享 store](#4. 父组件下两个子组件共享 store)
          • [父组件 index.vue](#父组件 index.vue)
          • [子组件 Child.vue(子组件A)](#子组件 Child.vue(子组件A))
          • [子组件 Child1.vue(子组件B)](#子组件 Child1.vue(子组件B))
  • [Slot 插槽内容分发](#Slot 插槽内容分发)
      • 原理说明
      • 使用场景
      • 代码示例
        • [1. SlotTest.vue(父组件)](#1. SlotTest.vue(父组件))
        • [2. Test.vue(子组件,包含默认插槽和具名插槽)](#2. Test.vue(子组件,包含默认插槽和具名插槽))
        • [3. Test1.vue(子组件,仅包含默认插槽)](#3. Test1.vue(子组件,仅包含默认插槽))

前言

组件通信是 Vue 开发中的核心知识点,也是初学者理解组件化思想的关键环节。在 Vue 应用中,组件并非孤立存在,它们之间需要通过数据传递、事件通知等方式协同工作,实现复杂的业务逻辑。

我系统整理了 Vue 中常用的组件通信方式,包括 props 父传子自定义事件子传父Event Bus 兄弟通信v-model 双向绑定$attrs 透传ref 获取组件实例provide/inject 跨级通信Pinia 全局状态管理Slot 插槽内容分发 共 9 种方案。

props 父传子

原理说明

props 是 Vue 组件间最基础、最常用的通信方式,核心遵循单向数据流 原则。具体来说,父组件可以通过在子组件标签上定义属性(props)的方式传递数据;子组件则需要通过 defineProps 函数明确声明接收这些数据。这里的单向数据流非常关键:子组件只能读取 props 中的数据,绝对不能直接修改,如果子组件需要更新数据,必须通过通知父组件,由父组件修改数据源后重新传递。这种机制保证了数据流向的清晰可追踪,避免了组件间数据混乱。

使用场景

适用于所有父组件向子组件传递数据的场景,例如:

  • 父组件将用户基本信息(姓名、年龄)传递给个人资料子组件展示;
  • 父组件将配置参数(如是否显示边框、主题颜色)传递给子组件控制样式;
  • 父组件将列表数据传递给子组件进行渲染展示。

代码示例

父组件 PropsTest.vue
vue 复制代码
<template>
  <div class="container">
    <h2>父组件</h2>
    <!-- 通过 fatherName(静态字符串)和 sonAge(响应式数据)两个 props 向子组件传递数据 -->
    <Son fatherName="Tom" :sonAge="age" />
  </div>
</template>

<script setup>
// 导入子组件
import Son from "./Child.vue";
// 导入 Vue 的 ref 函数创建响应式数据
import { ref } from "vue";
// 定义响应式变量 age,初始值为 12
const age = ref(12);
</script>

<style scoped>
.container {
  padding: 16px;
  background: #f5f5f5;
}
</style>

父组件通过两种方式传递 props:

  • fatherName="Tom":静态字符串传递,无需 v-bind: 是缩写);
  • :sonAge="age":响应式数据传递,必须用 v-bind 绑定,父组件数据更新时子组件会自动同步。
子组件 Child.vue
vue 复制代码
<template>
  <div class="child">
    <h3>子组件</h3>
    <!-- 直接使用接收的 props 数据渲染 -->
    <p>父亲名字: {{ fatherName }}</p>
    <p>儿子年龄: {{ sonAge }}</p>
    <!-- 尝试修改 props 的按钮 -->
    <button @click="tryModify">尝试修改 props</button>
  </div>
</template>

<script setup>
// 通过 defineProps 声明接收的 props 名称,参数是数组形式的 props 列表
const props = defineProps(['fatherName', 'sonAge']);
// 解构 props 中的数据,方便在模板和脚本中使用
const { fatherName, sonAge } = props;

// 尝试修改 props 的函数(实际无效)
const tryModify = () => {
  // 弹出提示:props 是只读的,直接修改会报错
  alert('props 是只读的,不能直接修改');
};
</script>

<style scoped>
.child {
  padding: 12px;
  background: #e0f7fa;
}
</style>

子组件核心逻辑说明:

  • 通过 defineProps(['fatherName', 'sonAge']) 明确声明需要接收的 props,确保数据来源清晰;
  • 解构 props 后可直接在模板中用 {``{ fatherName }} 渲染,或在脚本中使用;
  • tryModify 函数验证了 props 的只读性,直接修改(如 sonAge = 13)会触发 Vue 警告。

自定义事件 $emit 子传父

原理说明

当子组件需要向父组件传递数据或通知父组件执行操作时,可通过自定义事件实现。核心流程是:

  1. 子组件通过 defineEmits 提前声明要触发的自定义事件名称,明确事件类型;
  2. 子组件在特定时机(如按钮点击、数据变化)通过 emit 方法触发声明的事件,并可携带数据;
  3. 父组件在使用子组件时,通过 @事件名 监听子组件触发的事件,并在事件处理函数中接收子组件传递的数据。

这种方式实现了子组件到父组件的反向通信,是 Vue 中"子传父"的标准方案。

使用场景

适用于子组件有用户交互或内部状态变化需要通知父组件的场景,例如:

  • 子组件的表单提交按钮被点击,需要将表单数据传递给父组件保存;
  • 子组件的删除按钮被点击,需要通知父组件删除对应数据;
  • 子组件的下拉菜单选择项变化,需要将选中值传递给父组件。

代码示例

父组件 EventTest.vue
vue 复制代码
<template>
  <div>
    <!-- 监听子组件 UserForm 的自定义事件 submitUser,绑定处理函数 handleUserSubmit -->
    <UserForm @submitUser="handleUserSubmit" />
  </div>
</template>

<script setup>
// 导入子组件
import UserForm from './Event2.vue';

// 定义事件处理函数,参数 user 接收子组件传递的数据
const handleUserSubmit = (user) => {
  console.log('收到子组件提交的用户:', user); // 控制台输出子组件传递的用户信息
};
</script>

父组件核心逻辑:通过 @submitUser="handleUserSubmit" 监听子组件的 submitUser 事件,当子组件触发该事件时,handleUserSubmit 函数会被调用,参数即为子组件传递的数据。

子组件 Event2.vue
vue 复制代码
<template>
  <div class="form">
    <!-- 点击按钮触发 submit 函数 -->
    <button @click="submit">提交用户信息</button>
  </div>
</template>

<script setup>
// 通过 defineEmits 声明要触发的自定义事件,参数是数组形式的事件列表
const emit = defineEmits(['submitUser']);

// 按钮点击的处理函数
const submit = () => {
  // 子组件内部准备需要传递给父组件的数据
  const user = { name: 'Alice', age: 20 };
  // 触发自定义事件 submitUser,并传递 user 数据
  emit('submitUser', user);
};
</script>

<style scoped>
.form {
  padding: 12px;
  background: #fff3e0;
}
</style>

子组件核心逻辑:

  • defineEmits(['submitUser']) 声明要触发的事件 submitUser,确保事件来源可追溯;
  • submit 函数在按钮点击时执行,内部创建用户数据 user
  • 通过 emit('submitUser', user) 触发事件并传递数据,父组件的监听函数会接收该数据。

Event Bus 兄弟/跨层通信

原理说明

Event Bus(事件总线)是一种基于发布-订阅模式的跨组件通信方案,核心是创建一个全局的事件中心(通常是一个能触发和监听事件的对象)。具体流程:

  1. 所有组件都可以访问这个全局事件中心;
  2. 需要发送数据的组件(发布者)通过事件中心的 emit 方法发布事件,并携带数据;
  3. 需要接收数据的组件(订阅者)通过事件中心的 on 方法订阅对应事件,并在事件回调中处理数据;
  4. 组件销毁时需通过 off 方法取消订阅,避免内存泄漏(示例中简化未展示)。

在 Vue3 中,官方推荐使用 mitt 库实现事件总线(Vue2 中常用 Vue.prototype.$bus = new Vue(),但 Vue3 不再支持)。

使用场景

适用于无直接父子关系的组件间通信,例如:

  • 兄弟组件之间的通信(如页面左侧导航和右侧内容区的交互);
  • 跨多层级的组件通信(如孙子组件和祖父组件的通信,且中间层级无需关心数据);
  • 非嵌套关系的任意组件间数据传递。

代码示例

事件总线 bus/index.ts
js 复制代码
// 导入 mitt 库(需先通过 npm install mitt 安装)
import mitt from 'mitt';
// 创建 mitt 实例作为全局事件中心
const bus = mitt();
// 导出事件中心,供所有组件使用
export default bus;

这是事件总线的核心文件,创建了一个全局可访问的事件中心 bus,所有组件通过导入该 bus 实现通信。

兄弟组件通信示例
Child2.vue(发送事件的组件,发布者)
vue 复制代码
<script setup>
// 导入全局事件中心 bus
import bus from '../../bus';

// 定义发送数据的函数
const sendStudent = () => {
  // 通过 bus.emit 发布事件 updateStudent,并携带学生数据
  bus.emit('updateStudent', { name: 'Tom', grade: 3 });
};
</script>
<template>
  <div class="child">
    <!-- 点击按钮触发 sendStudent 函数,发布事件 -->
    <button @click="sendStudent">发送学生信息</button>
  </div>
</template>
<style scoped>
.child { padding: 12px; background: #e3f2fd; }
</style>

Child2 是数据的发送方:通过 bus.emit('事件名', 数据) 发布事件,其他组件可订阅该事件接收数据。

Child1.vue(接收事件的组件,订阅者)
vue 复制代码
<script setup>
// 导入全局事件中心 bus
import bus from '../../bus';
// 导入 Vue 的 onMounted 生命周期钩子,确保组件挂载后再订阅事件
import { onMounted } from 'vue';

// 组件挂载后执行
onMounted(() => {
  // 通过 bus.on 订阅 updateStudent 事件,回调函数接收发布者传递的数据
  bus.on('updateStudent', (student) => {
    console.log('收到学生信息:', student); // 控制台输出接收的数据
  });
});
</script>
<template>
  <div class="child">
    <p>等待接收学生信息...</p>
  </div>
</template>
<style scoped>
.child { padding: 12px; background: #fffde7; }
</style>

Child1 是数据的接收方:在 onMounted 生命周期中通过 bus.on('事件名', 回调函数) 订阅事件,当事件被发布时,回调函数会被触发并接收数据。

EventBusTest.vue(测试入口,父组件)
vue 复制代码
<template>
  <div class="container">
    <!-- 引入两个子组件,形成兄弟关系 -->
    <Child1 />
    <Child2 />
  </div>
</template>
<script setup>
// 导入两个子组件
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
</script>
<style scoped>
.container { display: flex; gap: 16px; } /* 横向排列两个子组件 */
</style>

该组件作为父组件,同时引入 Child1 和 Child2,使两者成为兄弟组件。点击 Child2 的按钮,Child1 会通过事件总线收到数据,实现兄弟组件通信。

v-model 父子双向绑定

原理说明

v-model 是 Vue 提供的父子组件双向数据绑定语法糖,本质是对"父传子 props + 子传父事件"的简化封装。核心流程:

  1. 父组件使用 v-model="数据" 绑定数据,等价于 :modelValue="数据" @update:modelValue="数据 = $event"
  2. 子组件通过 defineProps 接收 modelValue(默认名称,可自定义),作为展示的数据源;
  3. 子组件数据变化时,通过 emit('update:modelValue', 新数据) 触发事件,父组件会自动更新绑定的数据;
  4. 最终实现父子组件数据的实时同步,一方变化另一方自动更新。

使用场景

适用于需要父子组件数据实时同步的场景,例如:

  • 自定义输入框组件(输入内容实时同步到父组件);
  • 开关组件(开关状态在父子组件同步);
  • 滑块组件(滑动值实时反馈给父组件)。

代码示例

父组件 ModelTest.vue
vue 复制代码
<template>
  <div>
    <!-- v-model 绑定 personName,实现父子双向绑定 -->
    <PersonInput v-model="personName" />
  </div>
</template>
<script setup>
// 导入子组件
import PersonInput from './Child.vue';
// 导入 ref 创建响应式数据
import { ref } from 'vue';
// 定义响应式变量 personName,初始值为 'Tom'
const personName = ref('Tom');
</script>

父组件核心:v-model="personName" 等价于:

vue 复制代码
<PersonInput :modelValue="personName" @update:modelValue="personName = $event" />

无需手动写 props 和事件监听,简化了双向绑定的写法。

子组件 Child.vue
vue 复制代码
<template>
  <div class="input-box">
    <!-- 输入框的值绑定到 modelValue(父组件通过 v-model 传递) -->
    <input :value="modelValue" @input="onInput" />
    <!-- 按钮点击触发 updateName 函数 -->
    <button @click="updateName">修改名字</button>
  </div>
</template>
<script setup>
// 接收父组件通过 v-model 传递的 modelValue
const props = defineProps(['modelValue']);
// 声明要触发的 update:modelValue 事件
const emit = defineEmits(['update:modelValue']);

// 输入框输入事件处理函数
const onInput = (e) => {
  // 触发 update:modelValue 事件,传递输入框的最新值(e.target.value)
  emit('update:modelValue', e.target.value);
};
// 按钮点击修改名字的函数
const updateName = () => {
  // 主动触发事件,传递新值 'Jerry'
  emit('update:modelValue', 'Jerry');
};
</script>
<style scoped>
.input-box { padding: 12px; background: #f1f8e9; }
</style>

子组件核心逻辑:

  • 通过 defineProps(['modelValue']) 接收父组件传递的初始值;
  • 输入框 :value="modelValue" 绑定展示值,输入时触发 onInput 函数,通过 emit 传递新值更新父组件;
  • updateName 函数主动触发事件修改值,体现双向绑定的灵活性:无论是用户输入还是代码触发,都能同步到父组件。

attrs / listeners 属性和事件透传

原理说明

在组件嵌套层级较深时,父组件传递的属性和事件可能需要逐层传递到底层组件,$attrs 和属性透传机制可简化这一过程:

  • $attrs 是一个对象,包含父组件传递给子组件、但未被子组件通过 defineProps 声明接收的所有属性和事件(class 和 style 除外,它们会自动合并);
  • 子组件可通过 v-bind="$attrs"$attrs 中的所有属性和事件透传到内部的子组件(通常是原生 HTML 元素或基础组件);
  • 这种方式避免了中间组件手动声明大量 props 和事件,减少冗余代码。

Vue3 中 $listeners 已被合并到 $attrs 中,无需单独处理。

使用场景

适用于组件封装层级较深,需要透传属性和事件的场景,例如:

  • UI 组件库封装(如封装 Button 组件时,透传原生 button 的所有属性和事件);
  • 多层嵌套组件中,上层组件传递的属性需要直接作用于最底层组件。

代码示例

父组件 AttrsListenersTest.vue
vue 复制代码
<template>
  <div>
    <!-- 向 ActionButton 传递 type、label、@action 等属性和事件 -->
    <ActionButton type="primary" label="保存" @action="handleAction" />
  </div>
</template>
<script setup>
// 导入子组件
import ActionButton from './HintButton.vue';
// 定义 action 事件的处理函数
const handleAction = () => {
  alert('执行操作!'); // 点击按钮时触发
};
</script>

父组件向 ActionButton 传递了:

  • 属性 type="primary"(原生 button 的 type 属性);
  • 属性 label="保存"(自定义属性,用于按钮文本);
  • 事件 @action="handleAction"(自定义事件,按钮点击时触发)。
子组件 HintButton.vue
vue 复制代码
<template>
  <!-- 通过 v-bind="$attrs" 将 $attrs 中的属性和事件透传给 button 元素 -->
  <button v-bind="$attrs">{{ label }}</button>
</template>
<script setup>
// 导入 useAttrs 函数获取 $attrs 对象
import { useAttrs } from 'vue';
// 通过 defineProps 接收 label 属性(需要单独处理的属性)
const props = defineProps(['label']);
// 获取 $attrs 对象(包含未被 props 接收的属性和事件)
const $attrs = useAttrs();
</script>

子组件核心逻辑:

  • 通过 defineProps(['label']) 声明接收 label 属性,用于在模板中展示按钮文本 {``{ label }}
  • 未被 defineProps 接收的 type="primary"@action 事件会自动进入 $attrs
  • 通过 v-bind="$attrs"$attrs 中的属性和事件透传给原生 button 元素,最终:
    • button 会拥有 type="primary" 属性;
    • button 的点击事件会触发父组件的 handleAction 函数(因为 @action 事件被透传)。

这种方式下,中间组件(HintButton)无需声明 type@action,直接透传到底层 button 元素,减少代码冗余。

ref / parent / children 获取组件实例

原理说明

在某些场景下,父组件需要直接访问子组件的属性或调用子组件的方法,或子组件需要访问父组件的实例,可通过以下方式实现:

  • ref 获取子组件实例 :父组件在子组件标签上添加 ref="变量名",通过 变量名.value 获取子组件实例;子组件需通过 defineExpose 显式暴露需要被访问的属性和方法(默认情况下,setup 中的内容是私有的)。
  • ** p a r e n t 获取父组件实例 ∗ ∗ :子组件中通过 ' parent 获取父组件实例**:子组件中通过 ` parent获取父组件实例∗∗:子组件中通过'parent` 可直接获取父组件的实例,进而访问父组件的属性和方法(需注意层级关系,避免过度依赖导致耦合)。

这种方式直接操作组件实例,灵活性高,但会增加组件间的耦合度,需谨慎使用。

使用场景

适用于父组件需要直接控制子组件行为的场景,例如:

  • 父组件需要调用子组件的初始化方法或重置方法;
  • 子组件需要获取父组件的样式或状态(如示例中获取父组件的 class)。

代码示例

父组件 RefChildrenParentTest.vue
vue 复制代码
<template>
  <div class="container">
    <p>学生当前分数: {{ studentRef?.score }}</p> <!-- 通过 ref 访问子组件的 score 属性 -->
    <!-- 通过 ref="studentRef" 绑定 Son 组件实例 -->
    <Son ref="studentRef" />
    <!-- 点击按钮调用子组件的 study 方法 -->
    <button @click="callStudy">让学生学习</button>
    <!-- 通过 ref="daughterRef" 绑定 Daughter 组件实例 -->
    <Daughter ref="daughterRef" />
  </div>
</template>
<script setup>
// 导入子组件
import Son from './Son.vue';
import Daughter from './Daughter.vue';
// 导入 ref 创建用于绑定组件实例的变量
import { ref } from 'vue';
// 创建 ref 变量存储 Son 组件实例
const studentRef = ref();
// 创建 ref 变量存储 Daughter 组件实例(示例中未使用,仅展示绑定方式)
const daughterRef = ref();
// 定义调用子组件方法的函数
const callStudy = () => {
  // 通过 studentRef.value 获取子组件实例,调用其暴露的 study 方法
  studentRef.value.study();
};
</script>

<style scoped>
p{ 
  width: 200px;
  height: auto;
  background: #f0f0f0;
}
.container{
  width: 100vw;
  height:400px;
  padding: 10px;
  background: #b9dcea; /* 父组件的 class 样式 */
}
</style>

父组件核心逻辑:

  • 通过 ref="studentRef" 绑定 Son 组件,studentRef.value 即为 Son 组件的实例;
  • studentRef?.score 访问子组件暴露的 score 属性(?. 是可选链,避免未挂载时报错);
  • callStudy 函数通过 studentRef.value.study() 调用子组件暴露的 study 方法。
子组件 Son.vue
vue 复制代码
<template>
  <div class="student">
    <h3>学生分数: {{ score }}</h3> <!-- 展示 score 属性 -->
  </div>
</template>

<script setup>
// 导入 ref 创建响应式分数
import { ref } from 'vue';
// 定义分数属性(初始值 80)
const score = ref(80);
// 定义学习方法(调用时分数增加 5)
const study = () => {
  score.value += 5;
  console.log('学生正在学习,分数提升!');
};
// 通过 defineExpose 显式暴露 score 属性和 study 方法,供父组件访问
defineExpose({ score, study });
</script>

<style scoped>
.student {
  width: 200px;
  height: auto;
  background: #e0f7fa;
}
</style>

Son 组件核心:通过 defineExpose({ score, study })scorestudy 暴露给父组件,父组件才能通过 ref 访问,否则无法访问 setup 中的私有变量和方法。

子组件 Daughter.vue
vue 复制代码
<template>
  <div>
    <h2>父组件class: {{ parentClass }}</h2> <!-- 展示父组件的 class -->
    <!-- 点击按钮调用 getParentClass 函数,传递 $parent 获取父组件实例 -->
    <button @click="getParentClass($parent)">获取父组件class</button>
  </div>
</template>

<script setup>
// 导入 ref 存储父组件的 class
import { ref } from 'vue';
// 定义变量存储父组件的 class
const parentClass = ref('未知');
// 定义获取父组件 class 的函数
const getParentClass = ($parent) => {
  // 通过 $parent 获取父组件实例,$el 是组件的根 DOM 元素,className 是其 class 属性
  parentClass.value = $parent.$el.className;
};
</script>

<style scoped>
button {
  margin:10px 0 10px 0;
}
</style>

Daughter 组件核心:通过 $parent 获取父组件实例,$parent.$el 访问父组件的根 DOM 元素,进而获取其 class 属性,展示了子组件访问父组件实例的方式。

provide / inject 跨级通信

原理说明

provide/inject 是 Vue 提供的跨多层级组件通信方案,专门解决父子组件嵌套层级较深时,数据逐层传递(props 钻取)的问题:

  • 祖先组件通过 provide 方法提供数据(可以是响应式数据或普通值),指定一个注入名和对应的值;
  • 任意后代组件(无论层级多深)通过 inject 方法注入数据,使用注入名获取祖先组件提供的值;
  • 若提供的是响应式数据(如 refreactive 对象),后代组件修改数据会影响所有使用该数据的组件,实现跨层级数据同步。

使用场景

适用于跨多层级组件共享数据的场景,例如:

  • 全局配置(如主题颜色、语言设置)在所有组件中共享;
  • 权限信息在多层级组件中使用;
  • 框架级别的数据传递(如组件库中的上下文配置)。

代码示例

祖先组件 ProvideInjectTest.vue
vue 复制代码
<template>
  <div class="container">
    <h2>Provide/Inject 跨级通信示例</h2>
    <p>祖先组件 config: {{ config }}</p> <!-- 展示提供的 config 数据 -->
    <Child /> <!-- 引入子组件,形成层级:祖先 -> 子 -> 孙子 -->
  </div>
</template>

<script setup>
// 导入 ref 创建响应式数据,导入 provide 方法提供数据
import { ref, provide } from 'vue';
// 导入子组件
import Child from './Child.vue';
// 定义响应式 config 数据,初始主题为 light
const config = ref({ theme: 'light' });
// 通过 provide 提供数据,注入名为 'appConfig',值为 config
provide('appConfig', config);
</script>

<style scoped>
.container {
  width: 400px;
  min-height: 180px;
  background: #98d9ff; /* 初始主题颜色(light 模式) */
  padding: 20px;
}
</style>

祖先组件核心:通过 provide('appConfig', config) 提供数据,'appConfig' 是注入名(后代组件需用相同名称注入),config 是响应式数据,后代组件可获取并修改。

子组件 Child.vue
vue 复制代码
<template>
  <div class="child">
    <div>儿子</div>
    <GrandChild /> <!-- 引入孙子组件,形成更深层级 -->
  </div>
</template>

<script setup>
// 导入孙子组件
import GrandChild from './GrandChild.vue';
</script>

<style scoped>
.child {
  width: 350px;
  min-height: 120px;
  background: #67bced;
  padding: 16px;
}
</style>

该组件是中间层级,仅作为嵌套容器,无需处理 provide/inject 数据,体现了 provide/inject 跳过中间层级的优势。

孙子组件 GrandChild.vue
vue 复制代码
<template>
  <div class="grandchild">
    <div>孙子</div>
    <!-- 点击按钮切换主题 -->
    <button @click="updateTheme">切换主题</button>
  </div>
</template>

<script setup>
// 导入 inject 方法注入数据
import { inject } from 'vue';
// 通过 inject 注入祖先组件提供的 appConfig 数据,设置默认值防止未提供时为 undefined
const config = inject('appConfig', { value: { theme: 'light' } });
// 定义切换主题的函数
const updateTheme = () => {
  // 检查 config 是响应式数据(ref 对象),通过 .value 访问
  if (config && config.value) {
    // 切换主题(light <-> dark)
    config.value.theme = config.value.theme === 'light' ? 'dark' : 'light';
    // 根据主题修改祖先组件的背景色
    document.querySelector('.container').style.backgroundColor = config.value.theme === 'light' ? '#98d9ff' : '#135074';
  }
};
</script>
<style scoped>
.grandchild {
  margin:10px;
  padding: 8px;
  background: #98d9ff;
}
</style>

孙子组件核心:

  • 通过 inject('appConfig', 默认值) 获取祖先组件提供的 config 数据,注入名必须与 provide 时一致;
  • config 是响应式 ref 对象,通过 config.value 访问和修改其属性;
  • updateTheme 函数修改 config.value.theme,由于是响应式数据,所有使用该数据的组件(包括祖先组件)都会感知变化,实现跨层级数据同步。

Pinia 全局状态管理

原理说明

Pinia 是 Vue3 官方推荐的全局状态管理库,替代了 Vue2 中的 Vuex,核心优势是支持响应式、模块化和 TypeScript 类型推导。其工作原理:

  • 通过 defineStore 定义一个 store(仓库),包含 state(存储数据)、actions(修改数据的方法)等;
  • store 中的 state 是响应式的,任意组件获取 state 后,数据变化会触发组件重新渲染;
  • 组件通过导入 store 并调用其 stateactions,实现跨组件数据共享和修改;
  • 整个应用的状态集中管理,避免了组件间通信的繁琐,适合全局数据共享。

使用场景

适用于全局数据需要在多个组件间共享和同步的场景,例如:

  • 用户登录状态(用户名、权限)在所有组件中使用;
  • 购物车数据在商品列表、购物车页面、结算页面同步;
  • 全局计数器、通知消息等需要跨组件访问的数据。

代码示例

1. 安装 Pinia
bash 复制代码
npm install pinia

首先通过 npm 安装 Pinia 库,确保项目中可使用其 API。

2. 创建并注册 Pinia
js 复制代码
// src/store/index.js
import { createPinia } from 'pinia';
// 创建 Pinia 实例
const pinia = createPinia();
// 导出实例供 app 使用
export default pinia;

// main.ts
import { createApp } from 'vue';
import App from './App.vue';
// 导入 Pinia 实例
import pinia from './store';
// 创建 Vue 应用
const app = createApp(App);
// 应用 Pinia 插件
app.use(pinia);
// 挂载应用
app.mount('#app');

创建 Pinia 实例并通过 app.use(pinia) 注册到 Vue 应用,使整个应用都能使用 Pinia 的功能。

3. 定义 Store(全局计数器,js 版)
js 复制代码
// src/store/modules/info.js
import { defineStore } from 'pinia';

// 通过 defineStore 定义 store,第一个参数是唯一 id(需全局唯一),第二个参数是配置对象
export const useInfoStore = defineStore('info', {
  // state 是函数,返回初始状态对象
  state: () => ({
    count: 0 // 全局计数器,初始值 0
  }),
  // actions 是对象,包含修改 state 的方法(可异步)
  actions: {
    // 增加计数器的方法
    increment() {
      this.count++; // this 指向 store 实例,直接修改 state
    },
    // 减少计数器的方法
    decrement() {
      this.count--;
    }
  }
});

定义了一个名为 info 的 store,包含 count 状态和修改它的 incrementdecrement 方法,所有组件都可访问该 store。

4. 父组件下两个子组件共享 store
父组件 index.vue
vue 复制代码
<template>
  <div class="container">
    <h2>Pinia 父子组件共享状态示例</h2>
    <!-- 引入两个子组件,它们将共享同一个 store -->
    <Child />
    <Child1 />
  </div>
</template>
<script setup>
// 导入子组件
import Child from './Child.vue';
import Child1 from './Child1.vue';
</script>

父组件仅作为容器,引入两个子组件,两个子组件将通过 Pinia 共享全局状态。

子组件 Child.vue(子组件A)
vue 复制代码
<template>
  <div class="child">
    <h3>子组件A</h3>
    <p>全局计数: {{ infoStore.count }}</p> <!-- 展示 store 中的 count -->
    <!-- 点击按钮调用 store 的 increment 方法 -->
    <button @click="infoStore.increment">增加</button>
  </div>
</template>
<script setup>
// 导入定义的 store
import { useInfoStore } from '@/store/modules/info.js';
// 获取 store 实例
const infoStore = useInfoStore();
</script>

子组件A核心:通过 useInfoStore() 获取 store 实例,直接访问 infoStore.count 展示数据,点击按钮调用 increment 方法增加计数。

子组件 Child1.vue(子组件B)
vue 复制代码
<template>
  <div class="child">
    <h3>子组件B</h3>
    <p>全局计数: {{ infoStore.count }}</p> <!-- 展示同一个 store 的 count -->
    <!-- 点击按钮调用 store 的 decrement 方法 -->
    <button @click="infoStore.decrement">减少</button>
  </div>
</template>
<script setup>
// 导入定义的 store
import { useInfoStore } from '@/store/modules/info.js';
// 获取 store 实例(与子组件A的实例相同)
const infoStore = useInfoStore();
</script>

子组件B核心:同样通过 useInfoStore() 获取同一个 store 实例,展示的 count 与子组件A完全同步,点击按钮调用 decrement 方法减少计数。

这样,父组件下的两个子组件都能共享和操作同一个全局状态 count,实现响应式同步。无论哪个组件修改 count,另一个组件都会实时更新,体现了 Pinia 全局状态管理的优势。

Slot 插槽内容分发

原理说明

Slot(插槽)是 Vue 提供的组件内容分发机制,允许父组件向子组件的指定位置插入自定义内容,使子组件更灵活可定制。核心概念:

  • 默认插槽 :子组件中用 <slot></slot> 定义一个默认插入位置,父组件在子组件标签内的内容会默认插入到该位置;
  • 具名插槽 :子组件中用 <slot name="插槽名"></slot> 定义多个有名称的插槽,父组件通过 <template #插槽名> 指定内容插入到对应插槽;
  • 插槽内容由父组件提供,子组件负责定义插槽位置和样式,实现内容与结构的分离。

使用场景

适用于组件需要支持自定义内容的场景,例如:

  • 组件库开发(如卡片组件的头部、内容、底部可自定义);
  • 布局组件(如侧边栏、主内容区的内容由父组件指定);
  • 表单组件(如输入框前缀、后缀内容自定义)。

代码示例

1. SlotTest.vue(父组件)
vue 复制代码
<template>
  <div class="slot-container">
    <h2>Slot 插槽示例</h2>
    <!-- 使用 Test 组件,提供插槽内容 -->
    <Test>
      <!-- 默认插槽内容:通过 <template #default> 指定插入到 Test 组件的默认插槽 -->
      <template #default>
        <div class="slot-block">默认插槽内容</div>
      </template>
      <!-- 具名插槽内容:通过 <template #named> 指定插入到 Test 组件的 named 插槽 -->
      <template #named>
        <div class="slot-block">具名插槽内容</div>
      </template>
    </Test>
    <!-- 使用 Test1 组件,提供默认插槽内容 -->
    <Test1>
      <template #default>
        <div class="slot-block">Test1 默认插槽内容</div>
      </template>
    </Test1>
  </div>
</template>
<script setup>
// 导入子组件
import Test from './Test.vue';
import Test1 from './Test1.vue';
</script>
<style scoped>
.slot-container {
  width: 500px;
  min-height: 200px;
  background: #fbbaba;
  padding: 24px;
}
.slot-block {
  background: #fa676e;
  margin: 12px 0;
  padding: 16px;
  font-size: 16px;
}
</style>

父组件核心:通过 <template #插槽名> 为子组件的不同插槽提供内容,#default 可省略,直接在子组件标签内写内容即默认插槽。

2. Test.vue(子组件,包含默认插槽和具名插槽)
vue 复制代码
<template>
  <div class="test-block">
    <h3>Test 组件</h3>
    <!-- 默认插槽区域:父组件的 #default 内容插入到这里 -->
    <div class="slot-area">
      <slot></slot>
    </div>
    <!-- 具名插槽 named 区域:父组件的 #named 内容插入到这里 -->
    <div class="slot-area">
      <slot name="named"></slot>
    </div>
  </div>
</template>
<style scoped>
.test-block {
  background: #6ec2ff;
  padding: 18px;
  margin-bottom: 18px;
}
.slot-area {
  margin: 10px 0;
  padding: 10px;
  background: #c8fc8c; /* 插槽区域背景色,区分内容来源 */
}
</style>

Test 组件核心:定义了两个插槽:

  • <slot></slot>:默认插槽,接收父组件的 #default 内容;
  • <slot name="named"></slot>:具名插槽,接收父组件的 #named 内容;
  • 插槽所在的 .slot-area 定义了样式,使插入的内容有统一的展示风格。
3. Test1.vue(子组件,仅包含默认插槽)
vue 复制代码
<template>
  <div class="test1-block">
    <h3>Test1 组件</h3>
    <!-- 默认插槽区域:父组件的内容插入到这里 -->
    <div class="slot-area">
      <slot></slot>
    </div>
  </div>
</template>
<style scoped>
.test1-block {
  background: #f3d527;
  padding: 18px;
  margin-bottom: 18px;
}
.slot-area {
  margin: 10px 0;
  padding: 10px;
  background: #fff897; /* 插槽区域背景色 */
}
</style>

Test1 组件仅定义了默认插槽,父组件在其标签内的内容会默认插入到 <slot></slot> 位置,展示了默认插槽的基本用法。

通过以上示例,父组件可根据需求为子组件提供自定义内容,子组件通过插槽定义内容位置和样式,实现了组件的高复用性和灵活性。

相关推荐
snow每天都要好好学习20 分钟前
IC秋招刷题记录
笔记·面试
paopaokaka_luck27 分钟前
基于SpringBoot+Uniapp的非遗文化宣传小程序(AI问答、协同过滤算法、Echarts图形化分析)
java·vue.js·spring boot·后端·学习·小程序·uni-app
springfe010131 分钟前
vue3源码分析(1)一 环境搭建
vue.js·源码
lxmyzzs1 小时前
从 0 到 1 搞定nvidia 独显推流:硬件视频编码环境安装完整学习笔记
笔记·学习·音视频
LGGGGGQ1 小时前
嵌入式学习-土堆目标检测(2)-day26
学习
指间and流年1 小时前
生成式人工智能对网络安全的影响
笔记
灰海1 小时前
vscode,cursor,Trae终端不能使用cnpm、npm、pnpm命令解决方案
前端·ide·vue.js·vscode·npm·编辑器
谷新龙0011 小时前
Elasticsearch服务器开发(第2版) - 读书笔记 第一章 Elasticsearch集群入门
笔记·elasticsearch·mybatis
dolt022 小时前
关于el-table header 插槽踩坑记录
vue.js
Love__Tay2 小时前
金融工程、金融与经济学知识点
笔记·金融·经济学