Vue3 组件通信与 Pinia 高级
本页关键词:Getters、$subscribe、Store 组合式、props、自定义事件、mitt、v-model 组件、$attrs、$refs/$parent、provide/inject、插槽
一、Pinia 的 Getters:数据加工与计算属性
知识点讲解
Getters 的作用
当 state 中的数据需经处理后再使用时,可使用 getters 配置。Getters 类似于 Vue 组件中的 computed 计算属性:
- 对原始 state 进行逻辑加工
- 结果会被缓存,依赖未变化时不会重新计算
- 可在其他 getters 或组件中使用
Getters 的两种写法
1. 接收 state 参数(推荐用箭头函数)
import { defineStore } from 'pinia'
export const useCountStore = defineStore('count', {
state() {
return {
sum: 1,
school: 'atguigu'
}
},
getters: {
bigSum: (state): number => state.sum * 10
}
})2. 使用 this 访问(必须用普通函数)
getters: {
bigSum: (state): number => state.sum * 10,
upperSchool(): string {
return this.school.toUpperCase()
}
}使用 this 可访问 state、其他 getters、甚至 actions;必须用普通函数(箭头函数的 this 不指向 store)。
组件中使用 Getters
<template>
<div>
<h3>原始sum:{{ sum }}</h3>
<h3>放大后的sum:{{ bigSum }}</h3>
<h3>学校:{{ school }}</h3>
<h3>大写学校:{{ upperSchool }}</h3>
</div>
</template>
<script setup lang="ts">
import { useCountStore } from '@/store/count'
import { storeToRefs } from 'pinia'
const countStore = useCountStore()
let { sum, school, bigSum, upperSchool } = storeToRefs(countStore)
</script>二、$subscribe:状态变化监听与持久化
知识点讲解
通过 store 的 $subscribe() 方法可侦听 state 及其变化。常用于:数据持久化(同步到 localStorage)、日志记录、数据变化的副作用处理。
基本用法
import { useTalkStore } from '@/store/loveTalk'
const talkStore = useTalkStore()
talkStore.$subscribe((mutate, state) => {
console.log('LoveTalk', mutate, state)
localStorage.setItem('talk', JSON.stringify(state.talkList))
})参数说明:
mutate:变化的详细信息(type、storeId、events)state:变化后的完整 state 对象
三、Store 组合式(Setup)写法
知识点讲解
Pinia 支持组合式写法(Composition API),与 Vue3 的 <script setup> 风格一致,可直接使用 ref、reactive、computed。
核心规则:
- 使用
ref()或reactive()定义 state - 使用
computed()定义 getters - 使用
function定义 actions - 必须通过
return暴露需要给外部使用的属性和方法
完整示例
import { defineStore } from 'pinia'
import axios from 'axios'
import { nanoid } from 'nanoid'
import { reactive } from 'vue'
export const useTalkStore = defineStore('talk', () => {
const talkList = reactive(
JSON.parse(localStorage.getItem('talkList') as string) || []
)
async function getATalk() {
let { data: { content: title } } = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json')
let obj = { id: nanoid(), title }
talkList.unshift(obj)
}
return { talkList, getATalk }
})选项式 vs 组合式对照表
| 类型 | 选项式写法 | 组合式写法 |
|---|---|---|
| State | state() { return {...} } | ref() 或 reactive() |
| Getters | getters: {...} | computed() |
| Actions | actions: {...} | 普通 function |
| 暴露 | 自动暴露 | 需要 return { } |
使用 Vue 的 computed() API:
import { computed } from 'vue'
const bigSum = computed(() => sum.value * 10)
return { sum, bigSum }四、组件通信概述与变化
Vue3 组件通信的变化
| 变化类型 | Vue2 | Vue3 |
|---|---|---|
| 事件总线 | $bus | 移除,使用 mitt 代替 |
| 状态管理 | Vuex | 推荐使用 Pinia |
| 双向绑定 | v-model + .sync | 统一为 v-model(支持多个) |
| 属性透传 | $attrs + $listeners | 合并为 $attrs |
| 子组件访问 | $children | 移除,使用 ref |
常见通信方式与适用场景
| 方式 | 适用关系 | 说明 |
|---|---|---|
| props | 父 ↔ 子 | 最常用,父传子用数据,子传父用函数 |
| 自定义事件 | 子 → 父 | 子组件通过 emit 向父组件发送消息 |
| v-model | 父 ↔ 子 | 双向绑定语法糖,支持多个绑定 |
| $attrs | 祖 → 孙 | 属性透传,跨层级传递 props |
| $refs | 父 → 子 | 父组件直接访问子组件实例 |
| $parent | 子 → 父 | 子组件访问父组件实例 |
| provide/inject | 祖 ↔ 孙 | 依赖注入,祖先向后代提供数据 |
| mitt | 任意组件 | 事件总线,适合兄弟或远距离组件 |
| pinia | 任意组件 | 全局状态管理 |
| slot | 父 → 子 | 内容分发,父组件定制子组件结构 |
五、Props:最基础的父子通信
知识点讲解
Props 用于父子组件间的数据传递:
- 父传子:属性值是非函数(数据)
- 子传父:属性值是函数(回调)
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<h4>我的车:{{ car }}</h4>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child :car="car" :getToy="getToy"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
const car = ref('奔驰')
const toy = ref()
function getToy(value: string) {
toy.value = value
}
</script>子组件:
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<h4>父给我的车:{{ car }}</h4>
<button @click="getToy(toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref('奥特曼')
defineProps(['car', 'getToy'])
</script>六、自定义事件:更语义化的子传父
知识点讲解
自定义事件实现子 → 父通信,比 props 传函数更语义化。
关键区别:$event 的含义不同
| 事件类型 | $event 的含义 | 示例 |
|---|---|---|
| 原生事件 | 事件对象(pageX、pageY、target、keyCode 等) | @click="handler" |
| 自定义事件 | emit 触发时传递的数据(任意类型) | @send-toy="toy = $event" |
父组件:
<template>
<div class="father">
<h3>父组件</h3>
<h4>儿子给的玩具:{{ toy }}</h4>
<Child @send-toy="toy = $event"/>
</div>
</template>
<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref } from "vue";
const toy = ref('')
</script>子组件:
<template>
<div class="child">
<h3>子组件</h3>
<h4>我的玩具:{{ toy }}</h4>
<button @click="emit('send-toy', toy)">玩具给父亲</button>
</div>
</template>
<script setup lang="ts" name="Child">
import { ref } from "vue";
const toy = ref('奥特曼')
const emit = defineEmits(['send-toy'])
</script>七、Mitt:全局事件总线
知识点讲解
Mitt 是轻量级事件总线库,类似 Vue2 的 $bus,可实现任意组件间通信,适合兄弟组件、层级相隔较远的组件、不想用 Pinia 的简单场景。
安装与配置
npm i mitt新建 src/utils/emitter.ts:
import mitt from "mitt";
const emitter = mitt()
export default emitter使用流程
接收方 - 绑定事件:
<script setup lang="ts" name="Child2">
import emitter from "@/utils/emitter";
import { onUnmounted } from "vue";
import { ref } from "vue";
const toy = ref('')
emitter.on('send-toy', (value) => {
toy.value = value as string
})
onUnmounted(() => {
emitter.off('send-toy')
})
</script>发送方 - 触发事件:
<script setup lang="ts" name="Child1">
import emitter from "@/utils/emitter";
import { ref } from "vue";
const toy = ref('奥特曼')
function sendToy() {
emitter.emit('send-toy', toy.value)
}
</script>核心 API
| API | 说明 |
|---|---|
emitter.on(event, handler) | 绑定事件监听 |
emitter.emit(event, data) | 触发事件 |
emitter.off(event) | 解绑指定事件 |
emitter.all.clear() | 清空所有事件 |
面试要点:必须在组件卸载前解绑事件,否则导致内存泄漏和重复监听。
八、v-model:双向绑定的组件化应用
知识点讲解
在组件上使用 v-model
本质是 :modelValue + @update:modelValue:
<AtguiguInput v-model="userName"/>
<!-- 等价于 -->
<AtguiguInput
:modelValue="userName"
@update:modelValue="userName = $event"
/>子组件实现:
<template>
<div class="box">
<input
type="text"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
>
</div>
</template>
<script setup lang="ts" name="AtguiguInput">
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>自定义 v-model 参数名与多 v-model
<AtguiguInput v-model:abc="userName"/>
<!-- 子组件:defineProps(['abc'])、emit('update:abc', ...) --><AtguiguInput v-model:ming="userName" v-model:mima="password"/>面试要点:Vue3 的 v-model 支持自定义参数名和多个绑定;子组件不能直接修改 modelValue,必须通过 emit。
九、$attrs:属性透传机制
知识点讲解
$attrs 用于祖 → 孙通信。包含所有父组件传入的标签属性,自动排除 props 中声明的属性。可通过 v-bind="$attrs" 一次性透传。
中间层子组件:
<template>
<div class="child">
<GrandChild v-bind="$attrs"/>
</div>
</template>
<script setup lang="ts" name="Child">
import GrandChild from './GrandChild.vue'
</script>若子组件声明了 defineProps(['a', 'b']),则 $attrs 中不包含 a、b。
十、$refs 与 $parent:实例访问
知识点讲解
| 属性 | 方向 | 说明 |
|---|---|---|
$refs | 父 → 子 | 父组件访问子组件实例 |
$parent | 子 → 父 | 子组件访问父组件实例 |
子组件必须用 defineExpose 暴露数据;父组件通过 ref 属性标记子组件。$parent 在模板中直接使用,<script setup> 中不可用;会导致强耦合,不推荐频繁使用。
十一、provide 与 inject:依赖注入
知识点讲解
祖先通过 provide 提供数据,后代通过 inject 接收,中间层无需参与。
祖先:
provide('moneyContext', { money, updateMoney })
provide('car', car)后代:
let { money, updateMoney } = inject('moneyContext', {
money: 0,
updateMoney: (x: number) => {}
})
let car = inject('car')面试要点:provide 时必须传递
ref()或reactive()对象,不能传.value,否则失去响应式。
十二、插槽(Slot):内容分发机制
核心理念
数据在子组件,结构由父组件定。子组件提供数据或布局框架,父组件决定如何展示,实现高度可定制化。
默认插槽
父组件:
<Category title="今日热门游戏">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
</Category>子组件:
<template>
<div class="item">
<h3>{{ title }}</h3>
<slot></slot>
</div>
</template>具名插槽
父组件: v-slot:s1 或 #s1,必须用 <template> 包裹。
子组件: <slot name="s1"></slot>
作用域插槽(重点)
数据在子组件,展示结构由父组件决定。 子组件通过 :games="games" 向插槽传递数据,父组件用 v-slot="params" 或 #default="params" 接收。
子组件:
<slot :games="games" a="哈哈"></slot>父组件:
<Game v-slot="{ games, a }">
<ul>
<li v-for="g in games" :key="g.id">{{ g.name }}</li>
</ul>
<p>{{ a }}</p>
</Game>总结:组件通信方式速查表
| 通信方式 | 适用场景 | 数据方向 | 响应式 | 难度 |
|---|---|---|---|---|
| props | 父子组件 | 父 ↔ 子 | 是 | 低 |
| 自定义事件 | 父子组件 | 子 → 父 | - | 低 |
| v-model | 父子组件 | 父 ↔ 子 | 是 | 中 |
| $attrs | 祖孙组件 | 祖 → 孙 | 是 | 中 |
| $refs/$parent | 父子组件 | 父 ↔ 子 | 是 | 中 |
| provide/inject | 祖孙组件 | 祖 ↔ 孙 | 是 | 高 |
| mitt | 任意组件 | 任意方向 | - | 中 |
| pinia | 任意组件 | 全局共享 | 是 | 高 |
| slot | 父子组件 | 父 → 子 | 是 | 高 |