文章

Pinia状态管理原理

Pinia状态管理原理

一句话概括

Pinia 是 Vue 3 的官方状态管理库,本质就是用 Composition API 把全局的 reactive/ref 包装成 store,去掉了 Vuex 的 Mutation 层、去掉了 Module 嵌套,靠 Vue 3 的 Proxy 响应式系统 + TypeScript 类型推导,让状态管理退回到「写一个组合式函数,只不过作用域是全局的」。

核心知识点

1. 定义一个 Pinia Store

Option Store 和 Setup Store 两种写法,后者直接用 Composition API,推荐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Setup Store(推荐)——就是一个跑在全局作用域的 composable
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return { count, double, increment }
})

组件里使用:

1
2
3
const counter = useCounterStore()
// counter.count 是 ref,模板里自动解包
counter.increment() // 直接调 action,没有 dispatch/commit

2. 去掉了什么:Vuex vs Pinia

概念Vuex 4Pinia
Statestate: {}ref() / reactive()
Gettergetters: {}computed()
Mutationmutations: {}(必须)不存在
Actionactions: {} + dispatch就是普通函数直接调
Module嵌套 + namespace扁平结构
TS 推导需要手动声明类型自动推导
1
2
3
4
5
6
7
8
// Vuex 4 —— 改一个值要走 view → dispatch → action → commit → mutation
store.commit('user/SET_NAME', 'Tom')  // 带命名空间,容易拼错
store.dispatch('user/fetchProfile')

// Pinia —— 直接调
const user = useUserStore()
user.setName('Tom')        // 跟调普通函数一样
await user.fetchProfile()  // 异步也没区别

3. 为什么去掉了 Mutation

Vuex 2/3 时代保留 Mutation 是为了配合 Devtools 的时间旅行调试(每次 Mutation 都是一条可追踪的状态变更记录)。Vue 3 的 Proxy 响应式系统已经天然记录了所有的状态变更路径——pinia.state.value 的任意修改都会被追踪。Pinia 选择相信 Devtools 能直接拦截 Proxy 的 set trap 而不是依赖一个中间层。

真实数据:社区调研显示约 70% 的 Vuex Mutation 只是 state.xxx = payload,这种机械代码本就不该存在。

4. StoreToRefs —— 解构不丢响应式

1
2
3
4
5
6
7
8
9
10
11
12
const counter = useCounterStore()

// ❌ 直接解构丢响应式
const { count, double } = counter

// ✅ storeToRefs 保持响应式
import { storeToRefs } from 'pinia'
const { count, double } = storeToRefs(counter)
// count 仍然是 ref,模板里 .value 自动解包

// Action 不需要 storeToRefs
const { increment } = counter  // action 就是普通函数,不需要响应式

5. $patch 批量更新

1
2
3
4
5
6
7
8
9
// 对象形式
counter.$patch({ count: 10, name: 'new' })

// 函数形式 —— 一次 patch 内可以写复杂逻辑
counter.$patch((state) => {
  state.count++
  state.items.push(newItem)
})
// Devtools 中一条记录,而不是多条

「其实你每天都在用」

  1. 用户登录状态useUserStore 存 token + 用户信息,所有页面共享,Pinia 持久化插件自动存 localStorage。
  2. 购物车:添加/删除/修改数量都在 useCartStore 里,checkout 页面和购物车 icon 共享同一个 store。
  3. 多语言切换useI18nStorelocale,切换时所有页面响应式更新,不需要手动刷新。
  4. 暗色模式useThemeStoreisDark,切换时 body 的 class 自动变。不像 Vuex 时代要写 action → mutation → state。
  5. 全局 loading / toastuseAppStoreref 控制 loading 显隐,任意组件里 appStore.loading = false 即可。

常见误解(FAQ)

❌ 误区:Pinia 是 Vuex 的上位替代,所有项目都应该迁移。 如果你的 Vue 2 + Vuex 项目跑得好好的,不需要为了迁移而迁移。Pinia 的优势在 Vue 3 + TypeScript 项目里才能真正发挥。Vue 2 项目继续用 Vuex 完全没问题。

❌ 误区:Pinia 的 store 是全局单例。 是也不是。同一个 useFooStore() 在同一个应用实例内确实返回同一个 store 实例,但 Pinia 允许手动创建独立的 store 实例(createPinia()),适合测试和 SSR 请求隔离。

❌ 误区:Pinia 和 Vuex 一样是集中式状态树。 Pinia 不用单一状态树(single state tree),而是多个独立的 store。pinia.state.value 是一个按 store id 索引的 Map,而非一棵嵌套的树。这使得拆 store 毫无心理负担。

❌ 误区:不用 Mutation 就没法追踪状态变更了。 Devtools 直接拦截 Proxy 的 set 操作,Pinia 的 $subscribe 也能监听任意状态变更。追踪能力还在,只是不再是开发者在代码里手动 commit。

一句话总结

Pinia 把状态管理从「仪式感很重的架构」拉回到「能跑的组合式函数」——没有 Mutation、没有 dispatch、没有命名空间,你写的 store 就是一个普通的、全局作用域的 composable。

本文由作者按照 CC BY 4.0 进行授权