Pinia 状态管理详解
多个组件要共享同一份数据——登录用户信息、购物车、全局配置。Vue 2 时代的标准答案是 Vuex,Vue 3 时代的官方推荐换成了 Pinia(本文基于 Pinia 4.x,配 Vue 3)。名字看着像"菠萝",其实是
Pineapple(菠萝)+Vue的组合。
先还原一个真实场景:BinMaker 的后台系统里,侧边栏要显示当前登录人,顶栏要显示未读消息数,多个页面都要判断"是不是管理员"。这些数据如果每个组件各自存一份,改一处别处不同步,页面之间数据就"打架"了。
状态管理(Store)的职责就是:把跨组件共享的数据集中放一处,谁都能读、谁都能改、改了全通知。Vuex 干了这件事很多年,但用起来总有些别扭——于是 Vue 3 时代 Pinia 出现了。
版本演进:从 Vuex 到 Pinia
这是理解 Pinia 最好的切入点——Pinia 就是官方对 Vuex 多年痛点的回答。两条版本线要分清:
| 维度 | Vuex 3(配 Vue 2) | Vuex 4(配 Vue 3) | Pinia(配 Vue 3) |
|---|---|---|---|
| 修改状态 | 必须 commit(mutation) | 必须 commit(mutation) | 直接改 state,无需 mutation |
| 异步逻辑 | dispatch(action) | dispatch(action) | action 支持异步,直接调用 |
| 模块 | modules: { user: ... } 嵌套 | 同左 | 每个 store 平铺独立,无嵌套模块 |
| TypeScript | 支持差 | 支持差 | 原生 TS,类型推断完整 |
| 官方地位 | Vue 2 标配 | 过渡方案 | Vue 3 官方推荐 |
| 额外特性 | — | — | DevTools 时间旅行、storeToRefs、插件系统 |
为什么 Vuex 的 mutation 被砍掉?Vuex 当初模仿 Redux 引入 mutation 是为了"强制可追踪的变更",但实际开发里 action 里不写 mutation、直接改 state 的写法满天飞(Vuex 有 strict 模式但默认关)。Pinia 的立场是:DevTools 已经能追踪直接修改了,mutation 这层仪式感删掉,API 简化一半。
判断口诀:Vuex 改数据要
commit,Pinia 直接赋值。Vue 2 项目用 Vuex 3,Vue 3 老项目可能见到 Vuex 4,新项目一律 Pinia。
快速上手:第一个 store
// stores/counter.js —— Pinia 定义 store 的标准写法(Options 风格)
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'BinMaker' }), // 数据
getters: {
double: (state) => state.count * 2, // 派生数据(有缓存)
},
actions: {
increment(step = 1) { this.count += step; }, // 改数据的方法
},
});// main.js —— 必须用 app.use 安装
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const pinia = createPinia();
createApp(App).use(pinia).mount('#app');<!-- 组件里使用 -->
<script setup>
import { useCounterStore } from '../stores/counter';
const counter = useCounterStore(); // 获取 store 实例
counter.increment(); // 调 action 改数据
console.log(counter.count, counter.double);
</script>组件里用起来像用一个普通对象:读 counter.count、调 counter.increment(),数据变了所有用到它的组件自动更新。
核心概念只有四个
Pinia 的概念比 Vuex 少一半,就四个:
| 概念 | 作用 | 类比 |
|---|---|---|
| state | 数据本身 | 仓库里的货 |
| getters | 从 state 派生的计算值(有缓存) | 仓库的盘点视图 |
| actions | 修改数据的逻辑(可异步) | 出入库流程 |
| store | 上面三者的容器,按 id 唯一 | 一个独立仓库 |
state:直接改,不用 commit
实跑验证过的关键差异——Pinia 里直接赋值就生效:
counter.count = 100; // 直接改!Vuex 里这是禁止的,必须 commit响应式机制和 reactive() 同源,组件里改动即时触发视图更新。
getters:带缓存的派生值
getter 像 Vue 的 computed:依赖没变就不重算,多次读取走缓存。getter 之间还能互相引用:
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 2 }),
getters: {
double: (state) => state.count * 2, // 4
quad() { return this.double * 2; }, // 8 —— 通过 this 引用其他 getter
},
});actions:把"操作"打包,支持异步
action 里可以写任意逻辑——调接口、循环、组合多个 store:
export const useUserStore = defineStore('user', {
state: () => ({ profile: null, token: '' }),
actions: {
async login(username, password) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
this.token = res.token; // 直接改 state
return res.token;
},
logout() {
this.token = '';
this.profile = null;
},
},
});action 之间也可以互相调用:this.login(...) 或直接 useOtherStore()。
两种写法:Options 还是 Setup?
刚才用的是 Options 风格(state/getters/actions 平铺)。还有 Setup 风格,更接近组合式 API 的思维——用 ref/reactive 定义数据,用普通函数定义操作:
export const useCartStore = defineStore('cart', () => {
const items = ref(['资产1']);
const total = ref(1);
function addItem(item) {
items.value.push(item);
total.value++;
}
return { items, total, addItem }; // 返回给组件用
});| 风格 | 适合谁 |
|---|---|
| Options | 熟悉 Vuex 的老手,结构清晰 |
| Setup | 用组合式 API 的团队,逻辑可抽出复用 |
两种写法混用没问题(同一项目两个 store 可以各用各的风格),官方不强制统一。Setup 风格里 ref 会自动解包,组件里直接 cart.items 读、cart.addItem() 调。
最坑的一个点:解构丢响应性
从 store 里解构数据,这是新手最容易踩的坑:
const { count } = useCounterStore(); // ❌ 解构出来的 count 是普通值,改 store 它不变实跑验证:解构后改 counter.count = 200,解构出来的 count 还是旧值 100。因为 store 是 reactive 对象,解构把响应式代理拆丢了。
正确姿势是用官方提供的 storeToRefs:
import { storeToRefs } from 'pinia';
const { count } = storeToRefs(useCounterStore()); // ✅ 保持响应性
console.log(count.value); // 注意:是 ref,要 .value记忆口诀:storeToRefs 解 state 和 getters(变 ref),action 直接解构(函数不变响应式):
const counter = useCounterStore();
const { count, double } = storeToRefs(counter); // 响应式数据
const { increment } = counter; // 方法直接解store 之间怎么通信
A 的 action 里想动 B 的数据?直接调 B 的 store(实跑验证过):
export const useNotificationStore = defineStore('notification', {
state: () => ({ unread: 0 }),
actions: {
onNewMessage() {
const counter = useCounterStore(); // 直接拿另一个 store
counter.increment(); // 调它的 action
this.unread++;
},
},
});模块化思路和 Vuex 的 modules 不同:Pinia 不用嵌套模块,每个 store 平铺一个文件,互相引用。这比 Vuex 的嵌套模块树简单得多——不需要 namespaced 前缀,路径就是文件名。
插件机制:给所有 store 加能力
登录态、持久化这类"每个 store 都要"的能力,用插件一次注册全部生效:
// 插件:给所有 store 加一个 updatedAt 字段
const timestampPlugin = (context) => {
context.store.updatedAt = Date.now();
};
const pinia = createPinia();
pinia.use(timestampPlugin); // 注册后创建的 store 都生效注意:插件要在 app.use(pinia) 安装之后注册,或者注册后再创建 store(实跑踩坑:pinia.use() 在未安装时注册的插件要等 install 才生效)。
常见插件:pinia-plugin-persistedstate(状态持久化到 localStorage,刷新不丢)、pinia-plugin-debounce(防抖)。
总结:什么时候用 Pinia
| 场景 | 用不用 |
|---|---|
| 多个组件共享同一份数据(用户信息、配置) | 用,核心场景 |
| 组件内私有的临时数据 | 不用,本地 state 即可 |
| 只有父子两层传数据 | 不用,props/emit 更轻(见组件通信) |
| 页面多、共享数据多 | 用,按模块拆多个 store |
| Vue 2 老项目 | 用 Vuex 3,别硬上 Pinia(Pinia 要 Vue 3) |
版本提醒:Pinia 是 Vue 3 生态的官方状态管理,Vue 2 项目对应 Vuex 3;想完整了解 Vue 2 → Vue 3 的生态变迁,看 Vue 版本演进:Vue 2 与 Vue 3 全对比。
链式导航:Pinia 的响应式机制底层就是 Proxy,原理见 Vue 响应式原理详解;store 在组合式 API 里怎么组织,见 Composition API 详解;配 Vue Router 做权限控制(登录态存 Pinia、路由守卫读它),见 Vue Router 路由详解;想看这套概念落到真实项目(登录态 + 持久化 + 守卫),见 Pinia 实战:登录态 + 权限 + 持久化。
