跳到主要内容

Vuex 与 Pinia 对比详解

在 Vue 项目里,只要组件一多、页面一复杂,就很容易遇到这些问题:

  • 用户信息要在很多页面共享
  • 购物车、权限、主题、缓存列表要跨组件复用
  • 数据改动后,多个地方都要同步更新
  • 业务越来越大后,状态逻辑开始散落在各个组件里

这时候就会用到状态管理

Vue 生态里,最常被拿来比较的两个方案就是 VuexPinia。很多同学会问:

  • 它们到底有什么区别?
  • 新项目该选谁?
  • 老项目要不要迁移?
  • mutation 为什么在 Pinia 里没了?

这篇文章会把 Vuex 与 Pinia 的设计思路、使用方式、适用场景、迁移策略和面试重点 一次讲清楚。

1. 先给结论:怎么选最省事?

如果你只想先记住结论,可以先看这一段:

  • Vue 3 新项目:通常优先选择 Pinia
  • TypeScript 项目:通常优先选择 Pinia
  • Vuex 存量项目:如果项目稳定,通常没必要为了“追新”强行迁移
  • 团队已经深度使用 Vuex 且规范成熟:继续用 Vuex 也完全可以

一句话总结:Pinia 更轻、更自然、更适合现代 Vue 项目;Vuex 更经典、更传统、更常见于存量项目。

2. Vuex 和 Pinia 本质上都在解决什么问题?

先别急着记 API,先记它们的共同目标。

Vuex 和 Pinia 本质上都在做三件事:

  1. 集中管理共享状态:把多个组件都要用的数据放到统一位置
  2. 让状态变化可追踪:知道数据是谁改的、什么时候改的
  3. 减少组件间混乱通信:避免层层 props 传递和大量事件派发

比如下面这些数据,就很适合放到状态管理里:

  • 当前登录用户
  • 权限菜单
  • 购物车数据
  • 全局主题配置
  • 标签页缓存列表
  • 多页面共享的筛选条件

而像“某个输入框是否展开”“某个弹窗局部显示状态”这类只在单个组件内部使用的数据,通常没必要放全局状态。

判断标准

如果一份数据需要被多个页面或多个层级组件共享,或者它的变化需要被统一管理和追踪,就值得考虑交给 Vuex 或 Pinia。

3. 一张表看懂:Vuex vs Pinia

对比维度VuexPinia
设计年代感更传统、规则更强更现代、API 更轻
核心心智state + getters + mutations + actions + modulesstate + getters + actions + store
修改状态方式通常通过 commit 触发 mutation可在 action 中直接修改状态
异步处理action 异步,mutation 同步action 里直接处理同步/异步
模块拆分modules + 命名空间一个业务一个 store,更自然
TypeScript 体验相对繁琐更友好,推导更自然
模板/组件使用常见 mapStatemapGetters 等辅助函数直接 useXxxStore(),组合式风格更顺手
代码样板较多较少
DevTools 支持支持支持
学习成本略高略低
新项目倾向一般更少作为首选一般更常作为首选
存量项目情况很常见常见于新项目或迁移项目

如果把它们的差异翻译成人话:

  • Vuex 更像“流程更严格的管理制度”
  • Pinia 更像“规则保留核心部分,但写法更灵活”

4. 核心差异一:mutation 要不要单独存在?

这是两者最大的区别之一。

4.1 Vuex:修改状态要经过 mutation

在 Vuex 里,推荐的修改链路通常是:

组件 -> dispatch action -> commit mutation -> 修改 state

这样做的好处是:

  • 状态变更入口更统一
  • 每次修改都更容易追踪
  • 团队规范更清晰

但代价也很明显:

  • 简单功能也要写 actionmutation
  • 样板代码多
  • 新手容易觉得“绕”

4.2 Pinia:去掉 mutation

Pinia 的思路是:没有必要为了“改一个值”再额外包一层 mutation

你可以:

  • 直接在 action 里修改状态
  • 某些场景下直接写 store.count++

这样写起来会更直观:

  • 少一层跳转
  • 少一类概念
  • 代码更短,阅读成本更低
怎么理解这个变化

Vuex 更强调“所有修改都要走固定流程”;Pinia 更强调“保留可维护性,但减少不必要样板”。

5. 核心差异二:模块化方式不同

5.1 Vuex:通过 modules 管理大项目

Vuex 在大型项目里通常会把状态拆成多个模块:

  • user
  • permission
  • cart
  • app

然后在根 store 里注册这些模块。

这个方案没有问题,但常见痛点是:

  • 命名空间配置容易多
  • 调用路径容易变长
  • 类型推导不够自然

5.2 Pinia:天然就是“一个业务一个 store”

Pinia 的拆分方式更接近现代前端的思维:

  • 用户相关逻辑放 useUserStore
  • 购物车相关逻辑放 useCartStore
  • 权限相关逻辑放 usePermissionStore

也就是说,每个 store 天然就是一个独立模块,不需要再额外套一层 modules 概念。

这种方式有两个直接收益:

  • 目录结构更清晰
  • 业务边界更自然

6. 核心差异三:TypeScript 体验差距明显

如果你的项目使用 TypeScript,这一条通常非常关键。

6.1 Vuex 的痛点

Vuex 当然能配 TypeScript,但在真实项目里,大家常见的感受是:

  • 类型声明相对繁琐
  • commitdispatch 的类型约束写起来不够轻松
  • 模块多起来后,维护成本会提升

6.2 Pinia 更贴近组合式写法

Pinia 的 defineStore 写法和 Vue 3 的 Composition API 风格更接近,所以:

  • store 的 state/getters/actions 更容易推导类型
  • 组件内使用时也更自然
  • IDE 自动提示通常更顺手

对很多团队来说,这会直接影响开发效率。

7. 代码对比:同一个计数器功能分别怎么写?

只讲概念不够,我们直接看同一份需求:

需求很简单:

  • 有一个 count
  • 有一个派生值 doubleCount
  • 提供 increment
  • 提供一个异步获取用户信息的方法

7.1 Vuex 写法

import { createStore } from 'vuex'

type UserInfo = {
id: number
name: string
}

export default createStore({
state: () => ({
count: 0,
userInfo: null as UserInfo | null,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
mutations: {
increment(state) {
state.count += 1
},
setUserInfo(state, payload: UserInfo) {
state.userInfo = payload
},
},
actions: {
async fetchUser({ commit }) {
const user = await Promise.resolve({ id: 1, name: 'Terry' })
commit('setUserInfo', user)
},
},
})

在组件里通常这样用:

import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const count = computed(() => store.state.count)
const doubleCount = computed(() => store.getters.doubleCount)

function add() {
store.commit('increment')
}

7.2 Pinia 写法

import { defineStore } from 'pinia'

type UserInfo = {
id: number
name: string
}

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
userInfo: null as UserInfo | null,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1
},
async fetchUser() {
const user = await Promise.resolve({ id: 1, name: 'Terry' })
this.userInfo = user
},
},
})

在组件里通常这样用:

import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)

function add() {
counterStore.increment()
}

一眼就能看出:Pinia 少了 mutation 这一层,调用路径更短。

8. 核心差异四:组件里怎么用,差别也很明显

8.1 Vuex 常见写法

Vuex 时代大家常见这些辅助函数:

  • mapState
  • mapGetters
  • mapMutations
  • mapActions

它们不是不能用,但当业务复杂时,会让“数据来自哪里、调用的是谁”变得没那么直观。

8.2 Pinia 更贴近 Composition API

Pinia 一般就是:

  1. useXxxStore() 拿到 store
  2. 直接访问 state / getters / actions
  3. 解构响应式数据时用 storeToRefs()

这跟 Vue 3 项目整体的写法风格更统一。

一个高频坑

Pinia 中如果你直接这样写:

const { count } = counterStore

很容易把响应式“拆没了”。更稳妥的方式是对 state/getters 使用 storeToRefs(counterStore)

9. 核心差异五:学习成本与团队协作感受不同

9.1 Vuex 的优势:规则硬,团队边界明确

Vuex 的好处并不只是“老”。它真正的优势在于:

  • 流程固定
  • 修改链路统一
  • 大团队做严格约束时更容易达成一致

尤其是一些历史较长、成员较多、规范很强的项目,会更看重这种“强约束”。

9.2 Pinia 的优势:更少概念,更低上手门槛

Pinia 的优势是:

  • API 更少
  • 代码更短
  • 和 Vue 3 风格统一
  • 新同学更容易快速上手

所以从“学习成本”和“开发幸福感”看,Pinia 往往更占优。

10. 什么时候选 Vuex,什么时候选 Pinia?

10.1 适合优先考虑 Pinia 的场景

  • 新建 Vue 3 项目
  • 项目使用 TypeScript
  • 团队更偏向 Composition API
  • 想减少样板代码,提高开发效率
  • 希望 store 拆分更自然

10.2 适合继续使用 Vuex 的场景

  • 现有项目已经稳定运行多年
  • 团队对 Vuex 很熟,现有规范也围绕 Vuex 建立
  • 项目里已有大量 Vuex 模块、插件、中间层封装
  • 当前业务优先级不支持大规模迁移

10.3 最实用的判断原则

不要把“新”当成唯一标准,应该看三件事:

  1. 当前项目是不是新项目
  2. 团队是不是已经沉淀了 Vuex 规范
  3. 迁移收益是否真的大于迁移成本

11. 老项目要不要从 Vuex 迁移到 Pinia?

很多同学一看到 Pinia 更现代,就会问:那是不是应该马上迁?

答案通常是:不一定。

11.1 适合迁移的情况

  • 你本来就在做 Vue 3 升级
  • 你准备重构 store 结构
  • 现有 Vuex 代码冗长、维护成本高
  • 团队未来会持续走 TypeScript + Composition API 路线

11.2 不着急迁移的情况

  • Vuex 运行稳定,问题不大
  • 迁移期间风险较高
  • 业务节奏紧,没时间做系统性回归
  • 团队对 Pinia 还不熟,短期反而会增加成本

11.3 一个更稳妥的迁移思路

如果确实要迁,不建议“一晚上全量重写”,更建议:

  1. 先从新增模块开始使用 Pinia
  2. 再逐步替换低风险的 Vuex 模块
  3. 最后统一清理旧的辅助方法和封装

这种“渐进迁移”通常比一次性重构更稳。

12. 两者都要注意的通用问题

不管你用 Vuex 还是 Pinia,下面这些原则都很重要:

12.1 不要把所有数据都塞进全局状态

全局状态不是“公共垃圾桶”。

如果一个状态只在单个页面、单个弹窗里使用,就应该优先放组件内部。全局状态太多会让系统越来越重,也会增加排查成本。

12.2 不要把 store 当成后端接口文件夹

store 的重点是管理状态,而不是无限堆接口调用。更好的方式是:

  • 接口请求逻辑适当抽到 service
  • store 负责组织状态和业务动作

12.3 持久化要有边界

很多项目会把 token、主题、用户偏好做本地持久化,这是合理的;但不要无脑把所有状态都塞进 localStorage

你应该先问:

  • 这个数据真的需要刷新后保留吗?
  • 它会不会过期?
  • 会不会带来安全问题?

13. 面试时怎么回答“Vuex 和 Pinia 的区别”?

你可以用这套思路来回答:

Vuex 和 Pinia 都是 Vue 生态里的状态管理方案,核心目标都是集中管理共享状态。Vuex 的特点是流程更严格,强调通过 action 和 mutation 来修改 state;Pinia 去掉了 mutation,允许在 action 中直接修改状态,因此写法更轻、更贴近 Vue 3 的组合式风格。对 TypeScript 来说,Pinia 的类型推导通常也更自然。实际选型上,新项目一般更倾向 Pinia,而稳定运行的 Vuex 老项目不一定需要强行迁移。

这个回答的优点是:

  • 先说共同点
  • 再说核心差异
  • 最后落到选型建议

这样比单纯背 API 更像“真正理解了问题”。

14. 面试高频问答

Q1:Vuex 和 Pinia 的最大区别是什么?

参考回答:

最大的区别是 Pinia 去掉了 mutation。Vuex 通常要求通过 action -> mutation -> state 这条链路修改数据;Pinia 则可以直接在 action 中修改状态,因此代码更简洁,心智负担更低。


Q2:为什么很多新项目更愿意用 Pinia?

参考回答:

因为 Pinia API 更轻,和 Vue 3 的 Composition API 风格更统一,TypeScript 推导也通常更自然。对开发者来说,它的样板代码更少,上手更快,维护体验更好。


Q3:Vuex 一定比 Pinia 落后吗?

参考回答:

不一定。Vuex 并不是“不能用”,它只是设计更传统。对于已经稳定运行、团队规范成熟的老项目来说,Vuex 依然是可维护的方案。技术选型要看项目阶段、团队成本和迁移收益,而不是只看新旧。


Q4:Pinia 可以完全替代 Vuex 吗?

参考回答:

从很多日常业务场景看,Pinia 足以覆盖 Vuex 的主要使用需求,尤其适合现代 Vue 项目。但“能不能替代”还要结合现有项目架构、团队经验和迁移成本来判断。对新项目来说,Pinia 往往更合适;对老项目来说,不一定必须替换。


Q5:在 Pinia 里为什么解构时常配合 storeToRefs

参考回答:

因为直接从 store 上解构 state/getters,可能会丢失响应式连接。storeToRefs 会把这些响应式属性转换成 ref,这样在组件里解构后依然能保持响应式更新。

15. 最后总结

把这篇文章压缩成 3 句话,就是:

  1. Vuex 和 Pinia 都是状态管理工具,本质目标一致。
  2. Pinia 通过去掉 mutation、优化模块化和类型体验,让写法更现代。
  3. 新项目通常更倾向 Pinia,老项目则更应该看迁移成本而不是盲目重写。

如果你现在正在学 Vue,建议你优先把 “共享状态为什么需要集中管理” 这件事想透,再去记 Vuex 或 Pinia 的 API,理解会更稳,也更容易应对真实项目和面试。