Skip to content

D08 · Pinia vs Vuex ​

对应主课: L11 Pinia 状态管理 最后核对: 2026-09-23


1. 核心区别 ​

本篇比较 Vue 3 下的 Vuex 4.1.0 与 Pinia 3.0.4。Vuex 3 对应 Vue 2;Pinia 3 已移除 Vue 2 支持,不能混用旧版兼容结论。Pinia 3 迁移说明

维度Vuex 4Pinia 3
API 风格mutations + actions + getters只有 state + getters + actions
Mutations✅ 必须通过 mutation 修改❌ 不存在,直接修改 state
TypeScriptState/注入键可显式类型化,字符串接口较难推断函数与store属性推断更直接,边界仍需类型声明
模块化modules(嵌套 + 命名空间)多个独立 store(扁平)
DevTools✅ 支持✅ 支持
产物体积按具体构建测量按具体构建测量,不用固定数字代表所有项目
API 写法可通过 useStore 与 Composition API 配合支持 Setup Store、Option Store,也可用于 Options 组件
Store 热更新提供 hotUpdate 接口Vite 中使用 acceptHMRUpdate 接入

2. 同一功能对比 ​

以下文件用于独立的 Vue 3.5 / Vite / TypeScript 比较项目,不迁移前面已工作的商城 store。安装 pinia@3.0.4 vuex@4.1.0,两者共用同一个本地异步数据源;这里没有外部请求,重点观察 state/getter/action 的接口:

typescript
// src/services/items.ts
export async function loadItems(): Promise<string[]> {
  return ['Vue', 'Pinia']
}

Vuex 4.1.0 的包 exports 没有声明类型入口。现代 moduleResolution: 'Bundler' 若因此找不到声明,在比较项目的 tsconfig.app.json 保留其他设置、为 vuex 增加明确的类型解析路径;不要把模块整体声明成 any:

json
{
  "compilerOptions": {
    "paths": { "vuex": ["./node_modules/vuex/types/index.d.ts"] }
  }
}

如果原来已有 paths(例如 @/*),将 vuex 项合并进去。这只帮助 TypeScript 解析声明,运行时仍导入 Vuex 包。

Vuex ​

typescript
// src/store/legacy-counter.ts
import { createStore, type Store } from 'vuex'
import type { InjectionKey } from 'vue'
import { loadItems } from '../services/items'
export interface CounterState { count: number; items: string[] }
export const legacyStoreKey: InjectionKey<Store<CounterState>> = Symbol('legacy-counter')
export const legacyStore = createStore<CounterState>({
  state: () => ({ count: 0, items: [] }),
  getters: {
    doubleCount: state => state.count * 2,
    itemCount: state => state.items.length,
  },
  mutations: {
    INCREMENT(state) { state.count++ },
    SET_ITEMS(state, items: string[]) { state.items = items },
  },
  actions: {
    async fetchItems({ commit }) { commit('SET_ITEMS', await loadItems()) },
    increment({ commit }) { commit('INCREMENT') },
  },
})

Mutation 处理函数要求同步;异步获取放在 action,拿到结果后 commit。Vuex 的 strict 模式可检查 mutation 外的修改,但“只通过 mutation 修改”首先是使用契约,不能理解为 JavaScript 语法禁止给属性赋值。

Pinia ​

typescript
// src/stores/counter.ts
import { ref, computed } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { loadItems } from '../services/items'
export const useCounterStore = defineStore('counter-compare', () => {
  const count = ref(0)
  const items = ref<string[]>([])
  const doubleCount = computed(() => count.value * 2)
  const itemCount = computed(() => items.value.length)
  function increment() { count.value++ }
  async function fetchItems() { items.value = await loadItems() }
  return { count, items, doubleCount, itemCount, increment, fetchItems }
})
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}

Pinia 允许直接修改 state,也可使用 $patch 或 action;复杂业务仍应集中在清晰的操作接口中。上面的 HMR 是显式接入,新增 store 时也要考虑这段代码。Pinia HMR

注册与使用 ​

typescript
// src/main.ts(比较项目入口)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { legacyStore, legacyStoreKey } from './store/legacy-counter'
import CompareApp from './CompareApp.vue'
createApp(CompareApp).use(legacyStore, legacyStoreKey).use(createPinia()).mount('#app')
vue
<!-- src/CompareApp.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useStore } from 'vuex'
import { storeToRefs } from 'pinia'
import { legacyStoreKey } from './store/legacy-counter'
import { useCounterStore } from './stores/counter'
const legacy = useStore(legacyStoreKey)
const counter = useCounterStore()
const { count, doubleCount, itemCount } = storeToRefs(counter)
const error = ref('')
async function loadBoth() {
  error.value = ''
  try { await Promise.all([legacy.dispatch('fetchItems'), counter.fetchItems()]) }
  catch (cause) { error.value = cause instanceof Error ? cause.message : '加载失败' }
}
</script>
<template>
  <h1>状态管理接口对比</h1>
  <section>
    <h2>Vuex</h2>
    <p>{{ legacy.state.count }} / 两倍 {{ legacy.getters.doubleCount }} / 条目 {{ legacy.getters.itemCount }}</p>
    <button @click="legacy.commit('INCREMENT')">Vuex 增加</button>
  </section>
  <section>
    <h2>Pinia</h2>
    <p>{{ count }} / 两倍 {{ doubleCount }} / 条目 {{ itemCount }}</p>
    <button @click="counter.increment">Pinia 增加</button>
  </section>
  <button @click="loadBoth">加载两份列表</button>
  <p v-if="error" role="alert">{{ error }}</p>
</template>

两个计数器独立变化;加载后两边条目数都为 2。storeToRefs 保持解构后的 state/getter 连接,action 可以直接调用或解构。组件外调用 Pinia 的 useCounterStore 等函数时,必须保证 Pinia 已安装或显式传入该 Pinia 实例;SSR 要为每次请求创建独立实例,不能共享这份客户端示例的模块级状态。


3. 为什么去掉 Mutations ​

Vuex 用同步 mutation 记录状态变更;Pinia 也能记录 action 与直接状态修改,开发工具并不要求单独一层 mutation。去掉这一层以后:

  • 减少了样板代码
  • 普通操作通过导入的 store 函数和方法调用,减少字符串路径
  • 同步与异步逻辑都可放在 action,但异步状态与错误仍要处理

4. 模块化对比 ​

Vuex 模块可以 namespaced,调用例如 store.commit('user/SET_NAME', 'Vue');action context 提供当前模块 state 与 rootState。Pinia 用不同 id 定义 store,跨 store 操作可直接调用另一 store:

typescript
// src/stores/group-example.ts(独立的组织方式示例)
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('group-user', () => {
  const name = ref('')
  return { name }
})
export const useCartStore = defineStore('group-cart', () => {
  const items = ref<string[]>([])
  function describeCart() {
    const user = useUserStore()
    return { owner: user.name, items: [...items.value] }
  }
  return { items, describeCart }
})

在已安装 Pinia 的上下文调用;函数内读取另一 store 使依赖位置明确。扁平 store 不等于没有依赖管理:两个 setup store 不能在初始化时无条件互相读取状态,形成循环初始化。真实商城结算仍调用 L25 后端,不靠客户端描述函数完成交易。组合 store


5. TypeScript 支持 ​

Vuex 可以通过 createStore<CounterState> 与 InjectionKey<Store<CounterState>> 让 useStore 获得准确的 state 类型,§2 已展示完整接线;不能把一个没有类型参数的 useStore 作为“Vuex 只能 any”的依据。不过字符串 commit/dispatch/getters 的返回与载荷类型仍不如直接函数接口容易表达。Vuex TypeScript

Pinia 可从 ref、computed 和函数参数推断出 store 接口;空数组仍需 ref<string[]>([]),API 数据与联合状态仍要定义类型。Setup Store 应返回用于状态管理的 state,隐藏该状态可能破坏 SSR、devtools 或插件;普通函数内的临时局部变量则不必暴露。


6. 迁移建议 ​

建议
新项目✅ 直接用 Pinia
Vue 3 老项目用 Vuex 4按实际维护收益逐个模块迁移,不必一次重写
Vue 2 项目Pinia 3 不支持;先评估 Vue 3 迁移,旧生态版本要单独核对维护范围

Pinia 是 Vue 官方推荐的状态管理方案;Vuex 官方说明其处于维护模式。迁移时可以暂时并存,但一个业务状态应有明确的权威来源,避免两份 store 互相同步。Pinia 简介、Vuex 状态说明


7. 总结 ​

  • Pinia 去掉了 mutations,简化了开发流程
  • Pinia 用独立 store 替代嵌套 modules,更清晰
  • Pinia 提供更直接的类型推断,并同时支持 Setup Store 与 Option Store
  • Store HMR 需要按工具链接入;产物体积应以相同功能的实际构建为依据
  • 新 Vue 3 项目通常选择 Pinia;旧项目迁移时核对 Vue、状态库与业务状态的归属