Vuex 与 Pinia 对比详解
在 Vue 项目里,只要组件一多、页面一复杂,就很容易遇到这些问题:
- 用户信息要在很多页面共享
- 购物车、权限、主题、缓存列表要跨组件复用
- 数据改动后,多个地方都要同步更新
- 业务越来越大后,状态逻辑开始散落在各个组件里
这时候就会用到状态管理。
Vue 生态里,最常被拿来比较的两个方案就是 Vuex 和 Pinia。很多同学会问:
- 它们到底有什么区别?
- 新项目该选谁?
- 老项目要不要迁移?
mutation为什么在 Pinia 里没了?
这篇文章会把 Vuex 与 Pinia 的设计思路、使用方式、适用场景、迁移策略和面试重点 一次讲清楚。
1. 先给结论:怎么选最省事?
如果你只想先记住结论,可以先看这一段:
- Vue 3 新项目:通常优先选择
Pinia - TypeScript 项目:通常优先选择
Pinia - Vuex 存量项目:如果项目稳定,通常没必要为了“追新”强行迁移
- 团队已经深度使用 Vuex 且规范成熟:继续用 Vuex 也完全可以
一句话总结:Pinia 更轻、更自然、更适合现代 Vue 项目;Vuex 更经典、更传统、更常见于存量项目。
2. Vuex 和 Pinia 本质上都在解决什么问题?
先别急着记 API,先记它们的共同目标。
Vuex 和 Pinia 本质上都在做三件事:
- 集中管理共享状态:把多个组件都要用的数据放到统一位置
- 让状态变化可追踪:知道数据是谁改的、什么时候改的
- 减少组件间混乱通信:避免层层
props传递和大量事件派发
比如下面这些数据,就很适合放到状态管理里:
- 当前登录用户
- 权限菜单
- 购物车数据
- 全局主题配置
- 标签页缓存列表
- 多页面共享的筛选条件
而像“某个输入框是否展开”“某个弹窗局部显示状态”这类只在单个组件内部使用的数据,通常没必要放全局状态。
如果一份数据需要被多个页面或多个层级组件共享,或者它的变化需要被统一管理和追踪,就值得考虑交给 Vuex 或 Pinia。
3. 一张表看懂:Vuex vs Pinia
| 对比维度 | Vuex | Pinia |
|---|---|---|
| 设计年代感 | 更传统、规则更强 | 更现代、API 更轻 |
| 核心心智 | state + getters + mutations + actions + modules | state + getters + actions + store |
| 修改状态方式 | 通常通过 commit 触发 mutation | 可在 action 中直接修改状态 |
| 异步处理 | action 异步,mutation 同步 | action 里直接处理同步/异步 |
| 模块拆分 | modules + 命名空间 | 一个业务一个 store,更自然 |
| TypeScript 体验 | 相对繁琐 | 更友好,推导更自然 |
| 模板/组件使用 | 常见 mapState、mapGetters 等辅助函数 | 直接 useXxxStore(),组合式风格更顺手 |
| 代码样板 | 较多 | 较少 |
| DevTools 支持 | 支持 | 支持 |
| 学习成本 | 略高 | 略低 |
| 新项目倾向 | 一般更少作为首选 | 一般更常作为首选 |
| 存量项目情况 | 很常见 | 常见于新项目或迁移项目 |
如果把它们的差异翻译成人话:
- Vuex 更像“流程更严格的管理制度”
- Pinia 更像“规则保留核心部分,但写法更灵活”
4. 核心差异一:mutation 要不要单独存在?
这是两者最大的区别之一。
4.1 Vuex:修改状态要经过 mutation
在 Vuex 里,推荐的修改链路通常是:
组件 -> dispatch action -> commit mutation -> 修改 state
这样做的好处是:
- 状态变更入口更统一
- 每次修改都更容易追踪
- 团队规范更清晰
但代价也很明显:
- 简单功能也要写
action、mutation - 样板代码多
- 新手容易觉得“绕”
4.2 Pinia:去掉 mutation
Pinia 的思路是:没有必要为了“改一个值”再额外包一层 mutation。
你可以:
- 直接在
action里修改状态 - 某些场景下直接写
store.count++
这样写起来会更直观:
- 少一层跳转
- 少一类概念
- 代码更短,阅读成本更低
Vuex 更强调“所有修改都要走固定流程”;Pinia 更强调“保留可维护性,但减少不必要样板”。
5. 核心差异二:模块化方式不同
5.1 Vuex:通过 modules 管理大项目
Vuex 在大型项目里通常会把状态拆成多个模块:
userpermissioncartapp
然后在根 store 里注册这些模块。
这个方案没有问题,但常见痛点是:
- 命名空间配置容易多
- 调用路径容易变长
- 类型推导不够自然
5.2 Pinia:天然就是“一个业务一个 store”
Pinia 的拆分方式更接近现代前端的思维:
- 用户相关逻辑放
useUserStore - 购物车相关逻辑放
useCartStore - 权限相关逻辑放
usePermissionStore
也就是说,每个 store 天然就是一个独立模块,不需要再额外套一层 modules 概念。
这种方式有两个直接收益:
- 目录结构更清晰
- 业务边界更自然
6. 核心差异三:TypeScript 体验差距明显
如果你的项目使用 TypeScript,这一条通常非常关键。
6.1 Vuex 的痛点
Vuex 当然能配 TypeScript,但在真实项目里,大家常见的感受是:
- 类型声明相对繁琐
commit、dispatch的类型约束写起来不够轻松- 模块多起来后,维护成本会提升
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 时代大家常见这些辅助函数:
mapStatemapGettersmapMutationsmapActions
它们不是不能用,但当业务复杂时,会让“数据来自哪里、调用的是谁”变得没那么直观。
8.2 Pinia 更贴近 Composition API
Pinia 一般就是:
useXxxStore()拿到 store- 直接访问 state / getters / actions
- 解构响应式数据时用
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 最实用的判断原则
不要把“新”当成唯一标准,应该看三件事:
- 当前项目是不是新项目
- 团队是不是已经沉淀了 Vuex 规范
- 迁移收益是否真的大于迁移成本
11. 老项目要不要从 Vuex 迁移到 Pinia?
很多同学一看到 Pinia 更现代,就会问:那是不是应该马上迁?
答案通常是:不一定。
11.1 适合迁移的情况
- 你本来就在做 Vue 3 升级
- 你准备重构 store 结构
- 现有 Vuex 代码冗长、维护成本高
- 团队未来会持续走 TypeScript + Composition API 路线
11.2 不着急迁移的情况
- Vuex 运行稳定,问题不大
- 迁移期间风险较高
- 业务节奏紧,没时间做系统性回归
- 团队对 Pinia 还不熟,短期反而会增加成本
11.3 一个更稳妥的迁移思路
如果确实要迁,不建议“一晚上全量重写”,更建议:
- 先从新增模块开始使用 Pinia
- 再逐步替换低风险的 Vuex 模块
- 最后统一清理旧的辅助方法和封装
这种“渐进迁移”通常比一次性重构更稳。
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 句话,就是:
- Vuex 和 Pinia 都是状态管理工具,本质目标一致。
- Pinia 通过去掉
mutation、优化模块化和类型体验,让写法更现代。 - 新项目通常更倾向 Pinia,老项目则更应该看迁移成本而不是盲目重写。
如果你现在正在学 Vue,建议你优先把 “共享状态为什么需要集中管理” 这件事想透,再去记 Vuex 或 Pinia 的 API,理解会更稳,也更容易应对真实项目和面试。