目录
- 前言
- props 父传子
- 原理说明
- 使用场景
- 代码示例
- 父组件 PropsTest.vue
- 子组件 Child.vue
- 自定义事件 $emit 子传父
- 原理说明
- 使用场景
- 代码示例
- 父组件 EventTest.vue
- 子组件 Event2.vue
- Event Bus 兄弟/跨层通信
- 原理说明
- 使用场景
- 代码示例
- 事件总线 bus/index.ts
- 兄弟组件通信示例
- Child2.vue(发送事件的组件,发布者)
- Child1.vue(接收事件的组件,订阅者)
- EventBusTest.vue(测试入口,父组件)
- v-model 父子双向绑定
- 原理说明
- 使用场景
- 代码示例
- 父组件 ModelTest.vue
- 子组件 Child.vue
- \$attrs / $listeners 属性和事件透传
- 原理说明
- 使用场景
- 代码示例
- 父组件 AttrsListenersTest.vue
- 子组件 HintButton.vue
- ref / parent / children 获取组件实例
- 原理说明
- 使用场景
- 代码示例
- 父组件 RefChildrenParentTest.vue
- 子组件 Son.vue
- 子组件 Daughter.vue
- provide / inject 跨级通信
- 原理说明
- 使用场景
- 代码示例
- 祖先组件 ProvideInjectTest.vue
- 子组件 Child.vue
- 孙子组件 GrandChild.vue
- Pinia 全局状态管理
- 原理说明
- 使用场景
- 代码示例
- 1. 安装 Pinia
- 2. 创建并注册 Pinia
- 3. 定义 Store(全局计数器,js 版)
- 4. 父组件下两个子组件共享 store
- 父组件 index.vue
- 子组件 Child.vue(子组件A)
- 子组件 Child1.vue(子组件B)
- Slot 插槽内容分发
- 原理说明
- 使用场景
- 代码示例
- 1. SlotTest.vue(父组件)
- 2. Test.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
<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
<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 子传父
原理说明
当子组件需要向父组件传递数据或通知父组件执行操作时,可通过自定义事件实现。核心流程是:
- 子组件通过
defineEmits
提前声明要触发的自定义事件名称,明确事件类型; - 子组件在特定时机(如按钮点击、数据变化)通过
emit
方法触发声明的事件,并可携带数据; - 父组件在使用子组件时,通过
@事件名
监听子组件触发的事件,并在事件处理函数中接收子组件传递的数据。
这种方式实现了子组件到父组件的反向通信,是 Vue 中“子传父”的标准方案。
使用场景
适用于子组件有用户交互或内部状态变化需要通知父组件的场景,例如:
- 子组件的表单提交按钮被点击,需要将表单数据传递给父组件保存;
- 子组件的删除按钮被点击,需要通知父组件删除对应数据;
- 子组件的下拉菜单选择项变化,需要将选中值传递给父组件。
代码示例
父组件 EventTest.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
<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(事件总线)是一种基于发布-订阅模式的跨组件通信方案,核心是创建一个全局的事件中心(通常是一个能触发和监听事件的对象)。具体流程:
- 所有组件都可以访问这个全局事件中心;
- 需要发送数据的组件(发布者)通过事件中心的
emit
方法发布事件,并携带数据; - 需要接收数据的组件(订阅者)通过事件中心的
on
方法订阅对应事件,并在事件回调中处理数据; - 组件销毁时需通过
off
方法取消订阅,避免内存泄漏(示例中简化未展示)。
在 Vue3 中,官方推荐使用 mitt
库实现事件总线(Vue2 中常用 Vue.prototype.$bus = new Vue()
,但 Vue3 不再支持)。
使用场景
适用于无直接父子关系的组件间通信,例如:
- 兄弟组件之间的通信(如页面左侧导航和右侧内容区的交互);
- 跨多层级的组件通信(如孙子组件和祖父组件的通信,且中间层级无需关心数据);
- 非嵌套关系的任意组件间数据传递。
代码示例
事件总线 bus/index.ts
// 导入 mitt 库(需先通过 npm install mitt 安装)
import mitt from 'mitt';
// 创建 mitt 实例作为全局事件中心
const bus = mitt();
// 导出事件中心,供所有组件使用
export default bus;
这是事件总线的核心文件,创建了一个全局可访问的事件中心 bus
,所有组件通过导入该 bus
实现通信。
兄弟组件通信示例
Child2.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(接收事件的组件,订阅者)
<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(测试入口,父组件)
<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 + 子传父事件”的简化封装。核心流程:
- 父组件使用
v-model="数据"
绑定数据,等价于:modelValue="数据" @update:modelValue="数据 = $event"
; - 子组件通过
defineProps
接收modelValue
(默认名称,可自定义),作为展示的数据源; - 子组件数据变化时,通过
emit('update:modelValue', 新数据)
触发事件,父组件会自动更新绑定的数据; - 最终实现父子组件数据的实时同步,一方变化另一方自动更新。
使用场景
适用于需要父子组件数据实时同步的场景,例如:
- 自定义输入框组件(输入内容实时同步到父组件);
- 开关组件(开关状态在父子组件同步);
- 滑块组件(滑动值实时反馈给父组件)。
代码示例
父组件 ModelTest.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"
等价于:
<PersonInput :modelValue="personName" @update:modelValue="personName = $event" />
无需手动写 props 和事件监听,简化了双向绑定的写法。
子组件 Child.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
<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
<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
事件被透传)。
- button 会拥有
这种方式下,中间组件(HintButton)无需声明 type
和 @action
,直接透传到底层 button 元素,减少代码冗余。
ref / parent / children 获取组件实例
原理说明
在某些场景下,父组件需要直接访问子组件的属性或调用子组件的方法,或子组件需要访问父组件的实例,可通过以下方式实现:
- ref 获取子组件实例:父组件在子组件标签上添加
ref="变量名"
,通过变量名.value
获取子组件实例;子组件需通过defineExpose
显式暴露需要被访问的属性和方法(默认情况下,setup 中的内容是私有的)。 - **parent获取父组件实例∗∗:子组件中通过‘parent 获取父组件实例**:子组件中通过 `parent获取父组件实例∗∗:子组件中通过‘parent` 可直接获取父组件的实例,进而访问父组件的属性和方法(需注意层级关系,避免过度依赖导致耦合)。
这种方式直接操作组件实例,灵活性高,但会增加组件间的耦合度,需谨慎使用。
使用场景
适用于父组件需要直接控制子组件行为的场景,例如:
- 父组件需要调用子组件的初始化方法或重置方法;
- 子组件需要获取父组件的样式或状态(如示例中获取父组件的 class)。
代码示例
父组件 RefChildrenParentTest.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
<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 })
将 score
和 study
暴露给父组件,父组件才能通过 ref 访问,否则无法访问 setup 中的私有变量和方法。
子组件 Daughter.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
方法注入数据,使用注入名获取祖先组件提供的值; - 若提供的是响应式数据(如
ref
或reactive
对象),后代组件修改数据会影响所有使用该数据的组件,实现跨层级数据同步。
使用场景
适用于跨多层级组件共享数据的场景,例如:
- 全局配置(如主题颜色、语言设置)在所有组件中共享;
- 权限信息在多层级组件中使用;
- 框架级别的数据传递(如组件库中的上下文配置)。
代码示例
祖先组件 ProvideInjectTest.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
<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
<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
并调用其state
和actions
,实现跨组件数据共享和修改; - 整个应用的状态集中管理,避免了组件间通信的繁琐,适合全局数据共享。
使用场景
适用于全局数据需要在多个组件间共享和同步的场景,例如:
- 用户登录状态(用户名、权限)在所有组件中使用;
- 购物车数据在商品列表、购物车页面、结算页面同步;
- 全局计数器、通知消息等需要跨组件访问的数据。
代码示例
1. 安装 Pinia
npm install pinia
首先通过 npm 安装 Pinia 库,确保项目中可使用其 API。
2. 创建并注册 Pinia
// 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 版)
// 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
状态和修改它的 increment
、decrement
方法,所有组件都可访问该 store。
4. 父组件下两个子组件共享 store
父组件 index.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)
<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)
<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(父组件)
<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(子组件,包含默认插槽和具名插槽)
<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(子组件,仅包含默认插槽)
<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>
位置,展示了默认插槽的基本用法。
通过以上示例,父组件可根据需求为子组件提供自定义内容,子组件通过插槽定义内容位置和样式,实现了组件的高复用性和灵活性。