Skip to content

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

js
// 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; }, // 改数据的方法
  },
});
js
// 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');
html
<!-- 组件里使用 -->
<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 里直接赋值就生效:

js
counter.count = 100; // 直接改!Vuex 里这是禁止的,必须 commit

响应式机制和 reactive() 同源,组件里改动即时触发视图更新。

getters:带缓存的派生值

getter 像 Vue 的 computed:依赖没变就不重算,多次读取走缓存。getter 之间还能互相引用:

js
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:

js
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 定义数据,用普通函数定义操作:

js
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 里解构数据,这是新手最容易踩的坑:

js
const { count } = useCounterStore(); // ❌ 解构出来的 count 是普通值,改 store 它不变

实跑验证:解构后改 counter.count = 200,解构出来的 count 还是旧值 100。因为 store 是 reactive 对象,解构把响应式代理拆丢了。

正确姿势是用官方提供的 storeToRefs

js
import { storeToRefs } from 'pinia';

const { count } = storeToRefs(useCounterStore()); // ✅ 保持响应性
console.log(count.value); // 注意:是 ref,要 .value

记忆口诀storeToRefs 解 state 和 getters(变 ref),action 直接解构(函数不变响应式):

js
const counter = useCounterStore();
const { count, double } = storeToRefs(counter); // 响应式数据
const { increment } = counter;                  // 方法直接解

store 之间怎么通信

A 的 action 里想动 B 的数据?直接调 B 的 store(实跑验证过):

js
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 都要"的能力,用插件一次注册全部生效:

js
// 插件:给所有 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 实战:登录态 + 权限 + 持久化