前端面经-VUE3篇(四)--pinia篇-基本使用、store、state、getter、action、插件
一、基本使用
1、什么是 Pinia?
Pinia 是 Vue.js 的官方状态管理库,是 Vuex 的“升级版”。它专为 Vue 3 和 Composition API 设计,用于管理多个组件之间共享的数据(也叫“全局状态”)。
2、为什么需要状态管理库?
在 Vue 中,多个组件如果需要共享数据(例如登录用户信息、购物车内容、权限配置等),仅使用 props 和 emits 会变得非常复杂。这时就需要一个“中心仓库”来统一管理这些数据 —— 这就是 Pinia 的作用。
举个例子
假设你有一个购物车组件、一个商品列表组件和一个订单组件,都需要访问或修改购物车的内容。这种“共享数据”就是 Pinia 的用武之地。
在没有 Pinia 的情况下你可能需要这样做:
-
商品列表通过
emit
告诉父组件加入购物车 -
父组件再传递数据给购物车组件
-
数据很容易混乱且难以追踪
用 Pinia 后,这些组件可以直接访问全局共享的购物车数据,逻辑清晰,易于维护。
3、 基本使用
1. 配置 Pinia 到 Vue 应用在 main.js 或 main.ts 中引入并注册:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'const app = createApp(App)
const pinia = createPinia()app.use(pinia)
app.mount('#app')2. 创建一个 Store(状态模块)
你可以把每个业务逻辑写在一个独立的 store 文件中,例如:
// stores/counter.js 或 counter.ts
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {state: () => ({count: 0}),getters: {doubleCount: (state) => state.count * 2},actions: {increment() {this.count++}}
})
state: 用来定义数据getters: 类似于计算属性actions: 用来修改数据(支持异步)
3. 在组件中使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'const counter = useCounterStore()function addOne() {counter.increment()
}
</script><template><div>当前计数:{{ counter.count }}</div><div>双倍计数:{{ counter.doubleCount }}</div><button @click="addOne">+1</button>
</template>
二、store
1、什么是 Pinia 的 Store?
在 Pinia 中,store 是用来集中管理状态、逻辑和数据的地方。它就像是一个“全局的数据容器”,供多个 Vue 组件共享和操作。
每一个 store
都是独立的模块,可以包含:
-
state(状态数据)
-
getters(计算属性)
-
actions(行为函数)
Store 是用 defineStore()
定义的,它的第一个参数要求是一个独一无二的名字。这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use... 是一个符合组合式函数风格的约定。
import { defineStore } from 'pinia'// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {// 其他配置...
})
defineStore()
的第二个参数可接受两类值:Setup 函数或 Option 对象。
1、Option Store
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state
、actions
与 getters
属性的 Option 对象
export const useCounterStore = defineStore('counter', {state: () => ({ count: 0, name: 'Eduardo' }),getters: {doubleCount: (state) => state.count * 2,},actions: {increment() {this.count++},},
})
你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。
2、Setup Store
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
export const useCounterStore = defineStore('counter', () => {const count = ref(0)const doubleCount = computed(() => count.value * 2)function increment() {count.value++}return { count, doubleCount, increment }
})
在 Setup Store 中:
ref()
就是state
属性computed()
就是getters
function()
就是actions
注意,要让 pinia 正确识别 state
,你必须在 setup store 中返回 state
的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。
2、使用 Store
虽然我们前面定义了一个 store,但在我们使用 <script setup>
调用 useStore()
(或者使用 setup()
函数,像所有的组件那样) 之前,store 实例是不会被创建的:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>
请注意,store
是一个用 reactive
包装的对象,这意味着不需要在 getters 后面写 .value
。就像 setup
中的 props
一样,我们不能对它进行解构:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo" //
doubleCount // 将始终是 0 //
setTimeout(() => {store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
3、如何从 Pinia 的 store 中 解构变量 的同时 保持响应式
在 Vue 中,如果你这样写:const { name, doubleCount } = useCounterStore()
那么 name
和 doubleCount
会失去响应性,也就是说它们变成了普通变量,不再随着 store 的状态变化自动更新页面。
使用 storeToRefs():
import { storeToRefs } from 'pinia'const store = useCounterStore()
const { name, doubleCount } = storeToRefs(store)
storeToRefs()
会把 store 中的所有响应式属性(state
和 getters
)转换成 ref
对象,从而保留响应性。
const { increment } = store
这里可以直接解构 action,因为:
-
actions 本身就是函数
-
它们是绑定过
this
的,不需要ref
包装 -
所以解构时不会失效,直接拿来调用即可
4、总结
属性类型 | 是否需要 storeToRefs() | 是否保持响应性 | 是否可以解构 |
---|---|---|---|
state 数据 | ✅ 需要 | ✅ | ✅(通过 storeToRefs) |
getters | ✅ 需要 | ✅ | ✅(通过 storeToRefs) |
actions | ❌ 不需要 | ✅(始终) | ✅(直接解构) |
三、state
在 Pinia 中,state
就是你用来存储全局数据的地方。它相当于组件中的 data()
,但它可以被多个组件共享。
例如:
state: () => ({count: 0,user: {name: 'Xia',loggedIn: false}
})
这段代码定义了两个状态变量:count
和 user
,你可以在任何组件中使用和修改它们。
1、state
的三个关键点
1. 必须是一个函数(返回对象)
这是为了确保每个 store 实例有自己的状态(尤其是在 SSR 环境中)。
state: () => ({count: 0
})
2. 是响应式的
Pinia 内部使用 Vue 的响应式系统(reactivity API)来管理状态。这意味着当你修改 state
中的数据,使用它的组件会自动更新。
3. 可以直接读写
与 Vuex 不同,Pinia 的 state
变量可以直接修改,不需要通过 mutation。例如:
store.count++ // 这是合法的
store.user.name = 'Yang' // 也可以这样直接改
2、 state高级用法
1. 重置 state($reset()
方法)
作用
-
将
state
恢复为创建时定义的初始值。 -
适合用于退出登录、清空表单、重置计数器等场景。
使用方式
选项式 API 可直接使用 store.$reset()
。
const store = useStore()
store.$reset()
这个方法会自动调用最初定义的 state() 函数,用返回的对象替换当前的 state。
组合式 API 需要手动实现 $reset()
方法。
如果你使用的是 组合式 API(即 defineStore('id', () => {...})
格式),需要自己手动写一个 $reset()
方法:
export const useCounterStore = defineStore('counter', () => {const count = ref(0)function $reset() {count.value = 0}return { count, $reset }
})
2、$patch()
— 批量或复杂修改 state
作用
-
用于一次性更新多个 state 属性。
-
避免多次赋值产生性能问题,也方便调试(devtools 会合并记录)。
-
支持复杂的数组/对象操作。
用法
对象方式(简洁)
store.$patch({count: store.count + 1,name: 'DIO'
})函数方式(适合复杂修改)
store.$patch((state) => {state.items.push({ id: 1, name: 'shoes' })
})
$state
— 替换整个 state(实际上是 patch)
作用
-
用于初始化或从服务器、localStorage 恢复 state。
-
虽然看起来是“覆盖”,但实际上是调用了
$patch()
。
store.$state = { count: 24, name: 'Zhang' }
3、mapState()
— 把 state 映射为只读计算属性
作用
-
用于在选项式 API 的组件中使用 Pinia 的 state。
-
让
store.count
变成this.count
,方便模板访问。 -
本质上是一个只读
computed()
。
computed: {...mapState(useCounterStore, ['count'])
}
4、mapWritableState()
— 把 state 映射为可写计算属性
作用
-
和
mapState()
类似,但支持修改值。 -
适合表单绑定、v-model 等双向数据绑定场景。
computed: {...mapWritableState(useCounterStore, ['count'])
}
✅ 你可以在模板中用 v-model="count" 实现响应式输入。
5、$subscribe()
— 监听 state 的变化
作用
-
监听 store 的 state 改变,并触发回调。
-
常用于持久化(localStorage)、发送日志、自动保存等场景。
-
与 Vue 的
watch()
不同:$subscribe()
是 patch 触发一次,不是属性粒度触发。
store.$subscribe((mutation, state) => {localStorage.setItem('cart', JSON.stringify(state))
})
加上 { detached: true }
,让监听器在组件卸载后仍然有效(适合全局监听)。
6、watch(pinia.state)
— 监听整个应用状态(全局级别)
✅ 作用
-
用于在应用级别跟踪所有 store 的状态。
-
常用于初始化持久化系统或跨模块状态变化处理。
watch(pinia.state,(state) => {localStorage.setItem('piniaState', JSON.stringify(state))},{ deep: true }
)
小知识:
“初始化持久化系统”是指在 Vue 应用中,通过 Pinia 将全局状态(state)保存到浏览器的本地存储(如 localStorage),并在应用加载时恢复之前保存的状态,从而实现“断电重连”、“页面刷新不丢数据”等功能。
“跨模块状态变化处理”指的是:监听或响应多个 Pinia store 之间的状态变动,从而实现它们之间的协调、联动或同步逻辑。
为什么需要跨模块状态处理?
在实际开发中,你通常会为不同功能创建多个 store(模块化):
Store 模块 | 功能 |
---|---|
userStore | 管理登录用户信息 |
cartStore | 管理购物车内容 |
orderStore | 管理订单信息 |
uiStore | 控制 UI 状态(弹窗、loading 等) |
但这些模块的状态往往不是孤立的:
-
用户退出登录时,需要清空购物车、重置订单模块
-
添加商品到购物车后,UI 弹窗需要自动关闭
-
切换语言后,需要让其他模块响应更新
四、Getter
1、什么是 Getter?
在 Pinia 中,getter
就是计算属性(computed),用于从 state
中派生出一些数据,例如格式化、计算总和、过滤等。
它的作用就像:
-
Vue 组件中的
computed
-
Vuex 中的
getter
2、Getter 的作用
主要功能 | 举例说明 |
---|---|
派生出新值 | 例如 items.length 、count * 2 、是否登录 |
基于 state 但不会直接修改 state | 保证“只读逻辑” |
提高代码可读性 | 通过 getter 抽象逻辑,例如 isAdmin |
性能优化 | 只有依赖的值变化时才重新计算(和 computed 一样) |
Getter 的定义方式
在选项式 API中(推荐初学者)
export const useCounterStore = defineStore('counter', {state: () => ({count: 2}),getters: {doubleCount: (state) => state.count * 2,isEven: (state) => state.count % 2 === 0}
})在组合式 API中(进阶)
export const useCounterStore = defineStore('counter', () => {const count = ref(2)const doubleCount = computed(() => count.value * 2)return { count, doubleCount }
})
Getter 的使用方式
在组件中直接访问(不需要调用)
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script><template><p>原始:{{ counter.count }}</p><p>双倍:{{ counter.doubleCount }}</p>
</template>
✅ 注意:getter 就像计算属性,是自动响应式的,不需要加括号。✍️ Getter 可以访问其他 state、getter
getters: {isEven: (state) => state.count % 2 === 0,label: (state, getters) => getters.isEven ? '偶数' : '奇数'
}
3、Getter 是只读的
你不能直接修改 getter 的返回值:
store.doubleCount = 10 // ❌ 不允许,会报错
如果你需要修改值,请通过 action 或直接改 state。
1. 访问其他 Getter(组合 Getter)
当你想让一个 getter 基于另一个 getter 的结果继续计算,可以直接通过 this
访问其他 getter。
export const useCounterStore = defineStore('counter', {state: () => ({ count: 0 }),getters: {doubleCount(state) {return state.count * 2},doubleCountPlusOne(): number {return this.doubleCount + 1}}
})
💡 重点说明
this.doubleCount 是另一个 getter
如果你用 TypeScript,必须指定返回值类型,如 : number
2. 向 Getter 传递参数(返回一个函数)
❌ 错误方式(不能这样做)
// 不支持:getUserById(id) { ... }正确方式:返回一个函数
export const useUserListStore = defineStore('userList', {state: () => ({users: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]}),getters: {getUserById: (state) => {return (userId) => state.users.find((user) => user.id === userId)}}
})
注意
-
返回函数的 getter 不会被缓存(不像 computed),每次调用都重新执行
-
更适合用于 过滤、查找 等需要参数的场景
3. 缓存变体:先筛选再返回函数
getActiveUserById(state) {const activeUsers = state.users.filter((u) => u.active)return (id) => activeUsers.find((u) => u.id === id)
}
适合场景:
当你要多次按 id 查询某个子集(如活跃用户)
手动做了第一步过滤以节省后续运算
4. 访问其他 Store 的 Getter
import { useOtherStore } from './other-store'export const useStore = defineStore('main', {state: () => ({ localData: 10 }),getters: {otherGetter(state) {const otherStore = useOtherStore()return state.localData + otherStore.doubleValue // 使用别的 store 的 getter}}
})
你可以在 getter 中随意使用其他 store,就像在组件中那样要确保其他 store 已经定义好尽量避免循环依赖(Store A 调用 B,B 又回头调用 A)
五、Action
Action 相当于组件中的 method。它们可以通过 defineStore()
中的 actions
属性来定义,并且它们也是定义业务逻辑的完美选择。
类似 getter,action 也可通过 this
访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。不同的是,action
可以是异步的,你可以在它们里面 await
调用任何 API,以及其他 action!
1、定义
1. 使用选项式 API 定义 Store
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {state: () => ({count: 0}),actions: {increment() {this.count++}}
})
✅ 说明:
state: 返回一个对象,定义响应式数据 actions: 包含可以修改 state 的方法
这是 Pinia 的 选项式 API 写法,类似 Vue 组件的 data、methods🧩 2. 在组件中使用 store(有两种写法)
✅ 使用 setup()(组合式 API 风格)
import { useCounterStore } from '@/stores/counter'
export default defineComponent({setup() {const counterStore = useCounterStore()return { counterStore }},methods: {incrementAndPrint() {this.counterStore.increment()console.log('New Count:', this.counterStore.count)}}
})
📌 特点:
虽然这是在选项式组件中,但使用了 setup(),所以不需要 mapState() 或 mapActions() 映射
store 的属性都挂在 this.counterStore 上
✅ 不使用 setup(),使用 mapActions() 辅助函数
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'export default {methods: {...mapActions(useCounterStore, ['increment']),...mapActions(useCounterStore, { myOwnName: 'increment' })}
}特点:将 store 中的 action 映射为当前组件的 method
this.increment() 实际调用的是 store.increment()
如果你不使用 setup(),这是官方推荐的写法
2. 订阅 Action 的执行:$onAction()
✅ 作用:
$onAction()
允许你拦截 action 的执行过程,记录日志、添加监控、处理错误等。
const unsubscribe = store.$onAction(({ name, args, after, onError }) => {const start = Date.now()console.log(`Start "${name}" with args:`, args)after((result) => {console.log(`Finished "${name}" in ${Date.now() - start}ms. Result:`, result)})onError((err) => {console.warn(`Failed "${name}" in ${Date.now() - start}ms. Error:`, err)})
})
📌 回调参数说明:
参数 | 说明 |
---|---|
name | 当前执行的 action 名称 |
args | 调用该 action 时传入的参数 |
after(fn) | 在 action 成功执行后触发的回调 |
onError(fn) | 在 action 执行失败或抛错时触发的回调 |
3. 解绑监听器(防止内存泄露)
unsubscribe() // 手动移除订阅器
✅ 默认行为:
-
如果 store 是在组件的
setup()
中创建的,$onAction()
会自动在组件卸载时移除监听器。
✅ 永久监听(不随组件销毁):
store.$onAction(callback, true) // 第二个参数为 true
适用于:
-
全局日志系统
-
性能监控
-
状态操作历史记录
六、插件
1、什么是 Pinia 插件?
Pinia 插件是用来扩展所有 store 的功能的工具。它们可以:
-
添加默认属性
-
注入共享方法
-
实现状态持久化、本地缓存、日志记录等功能
可以把插件理解为:给每个 Store 加“外挂”功能,类似 Vue 插件或 Vuex 插件。
插件 = 给每个 store 加功能的小外挂。
想象一下你有很多个 store,就像很多个员工。你想让每个员工都多一个新技能,比如:
-
会自动保存工作成果(= 自动把 state 保存到 localStorage)
-
每次干活都写日志(= action 调用时打印日志)
-
都能说一句“你好老板”(= 添加一个公共方法)
你不用每次都手动写这段逻辑,而是通过 插件,一次性注入到所有 store 里,就像给每个员工配发一个新“工具包”。
2、插件能做什么?
Pinia 插件可以用来实现以下场景:
功能 | 示例 |
---|---|
状态持久化 | 保存 store 到 localStorage |
自动记录操作日志 | 记录每次 action 调用的时间、参数 |
注入公共方法 | 如 $toast() 、$confirm() |
初始化共享状态 | 比如全局主题、设置项 |
插件封装业务逻辑 | 例如 API 请求封装、授权检测 |
Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context。
export function myPiniaPlugin(context) {context.pinia // 用 `createPinia()` 创建的 pinia。context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。context.store // 该插件想扩展的 storecontext.options // 定义传给 `defineStore()` 的 store 的可选对象。// ...
}
然后用 pinia.use()
将这个函数传给 pinia
:
pinia.use(myPiniaPlugin)
插件只会应用于在 pinia
传递给应用后创建的 store,否则它们不会生效。
3、如何使用插件?
✅ 注册插件到 Pinia 实例
// main.js
import { createPinia } from 'pinia'
import piniaPlugin from './myPlugin'const pinia = createPinia()
pinia.use(piniaPlugin)
✅ 插件的基本结构
// myPlugin.js
export default function myPlugin({ store }) {// 添加一个自定义属性store.hello = 'world'// 添加一个共享方法store.sayHello = () => {console.log(`Hello from ${store.$id}`)}
}
使用后,你可以在任意 store 中访问:
const store = useMyStore()
store.sayHello() // 打印:Hello from myStore
4、插件能做的几件事(举例)
插件功能 | 作用说明 | 类比 |
---|---|---|
自动保存 state 到本地 | 页面刷新后还能恢复数据 | 自动保存工作成果 |
打印日志 | 每次调用 action 时记录操作 | 打卡上下班 |
增加自定义方法 | 比如 $toast() 弹窗提示 | 提供快捷按钮 |
修改 store 行为 | 比如封装通用校验 | 定制化员工行为 |
5、插件和 store 有什么区别?
Store 是什么? | 插件是干嘛的? |
---|---|
管理状态(数据)和操作(actions) | 扩展每个 store 的功能 |
每个业务写一个 | 全局使用一次,所有 store 都能用 |
比如 userStore , cartStore | 比如“保存功能”、“日志功能” |
6、插件的真正价值
插件的核心作用是:统一增强所有 store 的功能,而不是在每个 store 里重复写。
举个例子:
如果你希望所有模块的状态都能自动保存到本地(比如购物车、用户设置等),你肯定不想在每个 store 里写一遍 localStorage.setItem()
。
所以,你就写一个插件,让每个 store 自动保存它自己的数据,只要注册一次,全局生效。