Vue Pinia 状态管理入门资源
pinia官网:https://pinia.esm.dev/introduction.htmlvuex与pinia对比: https://segmentfault.com/a/1190000040368602pinia的简单使用: https://segmentfault.com/a/1190000040373313
(官网域名后来迁到了 https://pinia.vuejs.org,pinia.esm.dev 会跳转过去。)
两种写 store 的方式
Pinia 的 defineStore 支持两种风格,功能等价,选哪个看团队习惯。
Options 风格,结构上最接近 Vuex,迁移时改动最小:
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'wfly' }),
getters: {
double: state => state.count * 2,
},
actions: {
// 注意这里必须用普通函数,箭头函数会丢掉 this
increment() {
this.count++;
},
async fetchCount() {
const res = await fetch('/api/count');
this.count = (await res.json()).count;
},
},
});
Setup 风格,写法和组件的 setup() 完全一致:
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
// 必须把要暴露的东西都 return 出去,漏掉的外部访问不到
return { count, double, increment };
});
Setup 风格的好处是可以直接用组合式函数(useRoute、useLocalStorage 之类)和 watch,灵活度高。代价是 SSR 场景下要多留意,且 $reset() 默认不可用——Options 风格靠 state 那个工厂函数就能重建初始状态,Setup 风格 Pinia 不知道初始值是什么,需要自己实现:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
function $reset() {
count.value = 0;
}
return { count, $reset };
});
解构会丢响应性
这是新手最容易踩的坑。store 实例是一个 reactive 对象,直接解构会把响应式属性变成普通值:
const store = useCounterStore();
// 错:count 是一次性快照,store.count 变了这里不会更新
const { count } = store;
// 对:storeToRefs 会把 state 和 getter 都转成 ref
import { storeToRefs } from 'pinia';
const { count, double } = storeToRefs(store);
storeToRefs 只处理 state 和 getter,会跳过 action。action 是普通函数,解构出来不影响使用(Pinia 已经把 this 绑好了):
// action 直接解构没问题
const { increment } = store;
increment();
修改 state 的几种方式
const store = useCounterStore();
// 直接改,最常用
store.count++;
// 批量改,只触发一次订阅回调
store.$patch({ count: 10, name: 'new' });
// 函数式 patch,改数组/复杂结构时用这个
store.$patch(state => {
state.items.push({ id: 1 });
state.hasChanged = true;
});
// 整体替换(保留未提及的字段用 $patch,全量替换用 $state 赋值)
store.$state = { count: 0, name: 'wfly' };
// 重置为初始状态(仅 Options 风格开箱可用)
store.$reset();
对象形式的 $patch 是深合并,函数形式则给你直接操作 state 的机会。往数组里 push 大量元素时函数形式性能明显更好,因为对象形式需要 Pinia 遍历比对。
相比 Vuex 的实际差别
原文那篇对比文章值得看,我补几条在项目里体感最强的:
没有 mutation。 Vuex 要求同步修改走 mutation、异步走 action,实际写起来大量 mutation 只是一行赋值,纯样板代码。Pinia 只有 action,同步异步都行。
没有嵌套模块。 Vuex 的 modules 加上 namespaced: true 之后,dispatch('user/profile/fetch') 这种字符串路径既没有类型提示也容易写错。Pinia 是扁平的多 store,store 之间要互相引用就直接调用对方的 useXxxStore():
// stores/cart.js
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
actions: {
checkout() {
const user = useUserStore(); // 在 action 内部调用,此时 Pinia 已激活
if (!user.isLogin) return;
// ...
},
},
});
注意 useUserStore() 要写在 action 内部而不是模块顶层。写在顶层会在 app.use(pinia) 之前执行,报 getActivePinia() was called but there was no active Pinia。
TypeScript 类型自动推导。 Vuex 4 的类型支持基本靠手写 declare module 补,Pinia 不需要任何额外声明,state / getter / action 的类型全自动推出来。这是我从 Vuex 换过来最主要的动力。
体积更小,约 1.5kb。 且支持 tree shaking,没用到的 store 不会进产物。
常用的两个进阶点
订阅变化,做持久化或日志:
store.$subscribe((mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
localStorage.setItem('counter', JSON.stringify(state));
});
// 监听 action 的调用、成功和失败
store.$onAction(({ name, args, after, onError }) => {
after(result => console.log(`${name} 成功`, result));
onError(err => console.error(`${name} 失败`, err));
});
默认情况下组件卸载时订阅会被自动清理,想让它跟着 store 生命周期就传 { detached: true }。
插件机制,给所有 store 统一加能力:
// 给每个 store 注入一个 $router
pinia.use(({ store }) => {
store.$router = markRaw(router);
});
持久化通常不用自己写,pinia-plugin-persistedstate 已经做得很完整,配一个 persist: true 就行。

还没有留言,来发表第一条吧~