文章目录
- 前言
- 一、架构对比
-
- [1.1 数据流](#1.1 数据流)
- [1.2 核心差异一览](#1.2 核心差异一览)
- [二、Vuex 基本用法回顾](#二、Vuex 基本用法回顾)
-
- [2.1 创建 Store](#2.1 创建 Store)
- [2.2 组件中使用](#2.2 组件中使用)
- [2.3 模块化](#2.3 模块化)
- [三、Pinia 对应写法](#三、Pinia 对应写法)
-
- [3.1 同一功能的 Pinia 实现](#3.1 同一功能的 Pinia 实现)
- [3.2 模块化对比](#3.2 模块化对比)
- [四、API 对照表](#四、API 对照表)
- [五、为什么 Pinia 取代 Vuex](#五、为什么 Pinia 取代 Vuex)
-
- [5.1 去除 mutations 的冗余](#5.1 去除 mutations 的冗余)
- [5.2 更好的 TypeScript 支持](#5.2 更好的 TypeScript 支持)
- [5.3 多 Store 替代单 Store + 模块](#5.3 多 Store 替代单 Store + 模块)
- [5.4 原生 Composition API](#5.4 原生 Composition API)
- 六、迁移策略
-
- [6.1 何时迁移](#6.1 何时迁移)
- [6.2 渐进式迁移步骤](#6.2 渐进式迁移步骤)
- [6.3 迁移对照示例](#6.3 迁移对照示例)
- 七、面试聚焦
-
- [7.1 为什么 Pinia 取代 Vuex?](#7.1 为什么 Pinia 取代 Vuex?)
- [7.2 Vuex 的 mutations 为什么必须同步?](#7.2 Vuex 的 mutations 为什么必须同步?)
- [7.3 dispatch 和直接调用 action 的区别?](#7.3 dispatch 和直接调用 action 的区别?)
- 八、易混淆点
- 九、思考与练习
- 总结
前言
Vuex 是 Vue 2 时代的官方状态管理方案,Pinia 是其继任者,也是 Vue 3 的推荐选择。本篇会讲清楚:
- Vuex 与 Pinia 的架构差异
- API 与模块化设计对比
- 为什么 Pinia 取代 Vuex
- 从 Vuex 迁移到 Pinia 的策略
一、架构对比
1.1 数据流
Vuex:严格单向,修改 state 必须经过 mutations
组件 dispatch → actions(可异步)
↓
mutations(必须同步)
↓
state
↓
getters → 组件
Pinia:去掉 mutations,actions 直接修改 state
组件调用 → actions(同步/异步均可)
↓
state
↓
getters → 组件
1.2 核心差异一览
| 对比项 | Vuex | Pinia |
|---|---|---|
| Vue 版本 | Vue 2 / Vue 3 | Vue 3(推荐) |
| 修改 state | 必须 commit mutation | actions 中直接改 |
| mutations | 有,必须同步 | 无 |
| 模块化 | 单 Store + 嵌套 modules | 多个独立 Store |
| 命名空间 | 需 namespaced: true |
天然隔离,无需配置 |
| TypeScript | 需复杂类型声明 | 天然支持,完整推导 |
| DevTools | 支持 | 支持,集成更直观 |
| 体积 | 较大 | 更轻量(约 1KB) |
| Composition API | 支持但体验一般 | 原生 Setup Store |
二、Vuex 基本用法回顾
2.1 创建 Store
javascript
// store/index.js
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0,
user: { name: '', token: '' }
},
getters: {
doubleCount: (state) => state.count * 2
},
mutations: {
INCREMENT(state) {
state.count++ // mutations 必须同步
},
SET_USER(state, user) {
state.user = user
}
},
actions: {
async login({ commit }, credentials) {
const res = await api.login(credentials)
commit('SET_USER', res.data) // 通过 commit 调用 mutation
},
increment({ commit }) {
commit('INCREMENT')
}
}
})
2.2 组件中使用
javascript
import { useStore } from 'vuex'
import { computed } from 'vue'
const store = useStore()
// 读取
store.state.count
store.getters.doubleCount
// 修改
store.commit('INCREMENT')
store.dispatch('login', { username, password })
2.3 模块化
javascript
// store/modules/user.js
export default {
namespaced: true,
state: () => ({ token: '' }),
mutations: {
SET_TOKEN(state, token) { state.token = token }
},
actions: {
login({ commit }, form) {
commit('SET_TOKEN', 'xxx')
}
}
}
// store/index.js
import user from './modules/user'
import cart from './modules/cart'
export default createStore({
modules: { user, cart }
})
// 组件中
store.dispatch('user/login', form)
store.state.user.token
三、Pinia 对应写法
3.1 同一功能的 Pinia 实现
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++ // 直接修改,无需 mutation
},
async login(credentials) {
const res = await api.login(credentials)
this.user = res.data // 异步 action 中直接改 state
}
}
})
javascript
// 组件中
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)
store.increment()
await store.login(form)
3.2 模块化对比
javascript
// Vuex:单 Store + 嵌套 modules
store.state.user.token
store.dispatch('user/login')
// Pinia:多个独立 Store,按文件拆分
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
Pinia 每个 Store 即一个模块,无需 namespaced、无需在根 Store 注册 modules。
四、API 对照表
| 操作 | Vuex | Pinia |
|---|---|---|
| 读取 state | store.state.xxx |
store.xxx |
| 读取 getter | store.getters.xxx |
store.xxx |
| 同步改 state | store.commit('MUTATION', payload) |
store.action() 或直接改 |
| 异步操作 | store.dispatch('action', payload) |
store.action() |
| 批量修改 | 多个 commit | store.$patch({ ... }) |
| 监听变化 | store.watch() |
store.$subscribe() |
| 重置 state | 无内置 | store.$reset() |
| 组合式用法 | useStore() + mapXxx |
useXxxStore() + storeToRefs |
五、为什么 Pinia 取代 Vuex
5.1 去除 mutations 的冗余
Vuex 的 mutations 存在意义是 DevTools 时间旅行调试时能记录每次 state 变更。但实际开发中:
- 简单修改也要写 mutation + action 两层,样板代码多
- 异步场景必须在 action 里 commit mutation,流程繁琐
Pinia 去掉 mutations,actions 直接改 state,DevTools 仍能追踪变更。
5.2 更好的 TypeScript 支持
typescript
// Vuex:需手动定义 State、Getters、Actions 类型,较繁琐
// Pinia:defineStore 自动推导,几乎零配置
export const useUserStore = defineStore('user', {
state: (): { name: string } => ({ name: '' }),
actions: {
setName(name: string) { this.name = name } // this 有完整类型
}
})
5.3 多 Store 替代单 Store + 模块
Vuex 所有状态在一棵 state 树下,大型项目 modules 嵌套深、命名空间冗长。Pinia 按业务域拆成独立 Store 文件,职责清晰、按需引入。
5.4 原生 Composition API
Pinia 的 Setup Store 与 ref / computed 完全一致,可复用 composable,Vuex 4 虽支持 Vue 3 但体验仍偏 Options API。
六、迁移策略
6.1 何时迁移
| 场景 | 建议 |
|---|---|
| 新项目 | 直接用 Pinia |
| Vue 2 老项目 | 继续 Vuex,除非升级 Vue 3 |
| Vue 3 新项目用了 Vuex | 建议逐步迁 Pinia |
| Vue 3 大型老项目 Vuex | 渐进式迁移,按模块逐个替换 |
6.2 渐进式迁移步骤
第一步:安装 Pinia,与 Vuex 共存
javascript
import { createPinia } from 'pinia'
import store from './store' // 旧 Vuex
app.use(store)
app.use(createPinia())
第二步:新功能用 Pinia,旧模块保持 Vuex
新页面、新 Store 用 Pinia 编写,不影响现有 Vuex 模块。
第三步:按模块逐个迁移
javascript
// Vuex module: store/modules/user.js
// ↓ 迁移为
// Pinia store: stores/user.js
// 对照关系:
// state → state / ref
// getters → getters / computed
// mutations → 合并到 actions
// actions → actions
// namespaced → 删除,独立 defineStore
第四步:组件中替换引用
javascript
// 迁移前
import { useStore } from 'vuex'
const store = useStore()
store.dispatch('user/login', form)
store.state.user.token
// 迁移后
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
await userStore.login(form)
userStore.token
第五步:移除 Vuex
所有模块迁移完成后,删除 Vuex 依赖和 store/index.js。
6.3 迁移对照示例
javascript
// ===== Vuex =====
// store/modules/cart.js
export default {
namespaced: true,
state: () => ({ items: [] }),
mutations: {
ADD_ITEM(state, item) { state.items.push(item) }
},
actions: {
addItem({ commit }, item) {
commit('ADD_ITEM', item)
}
},
getters: {
total: (state) => state.items.length
}
}
// ===== Pinia =====
// stores/cart.js
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: {
total: (state) => state.items.length
},
actions: {
addItem(item) {
this.items.push(item) // mutation 逻辑直接放 action
}
}
})
没有官方自动化迁移工具,需手动逐个模块重构。
七、面试聚焦
7.1 为什么 Pinia 取代 Vuex?
- 去掉 mutations,API 更简洁
- 多 Store 替代单 Store + 嵌套模块
- 原生 TypeScript 和 Composition API 支持
- 更轻量,DevTools 体验更好
7.2 Vuex 的 mutations 为什么必须同步?
DevTools 需要记录每次 state 变更的快照做时间旅行。若 mutation 异步,变更时机不可追踪,调试会混乱。Pinia 通过其他机制实现 DevTools 追踪,不再需要 mutations 层。
7.3 dispatch 和直接调用 action 的区别?
- Vuex :
dispatch('action')返回 Promise,统一入口,可触发订阅 - Pinia :actions 就是普通方法,直接
store.action(),返回值由函数自身决定
八、易混淆点
- Pinia 不是 Vuex 的简单封装:架构重新设计,不是换皮。
- Vuex mutations 必须同步:异步逻辑放 actions,通过 commit 改 state。
- Pinia 无 namespaced :每个 Store 天然独立,用
useXxxStore()按需引入。 - 两者可共存:迁移期间 Vuex 和 Pinia 可同时使用。
- Vuex 4 仍可用于 Vue 3:但官方推荐 Pinia,新项目不应再选 Vuex。
九、思考与练习
1. Vuex 和 Pinia 的数据流有什么区别?
解析:Vuex 是 state → mutations → actions,改 state 必须 commit mutation;Pinia 去掉 mutations,actions 直接修改 state。
2. 为什么 Vuex 需要 mutations?
解析:保证 state 变更可追踪,配合 DevTools 时间旅行调试;mutations 必须同步以确保快照准确。
3. Pinia 如何实现模块化?
解析:每个业务域一个独立 Store 文件(defineStore),无需根 Store 注册 modules,无需 namespaced。
4. 从 Vuex 迁移 Pinia 的核心步骤?
解析:安装 Pinia 共存 → 新功能用 Pinia → 按模块逐个迁移(mutations 合并到 actions)→ 组件替换引用 → 移除 Vuex。
5. 新项目该选 Vuex 还是 Pinia?
解析:选 Pinia。API 更简洁、TS 更好、官方推荐,Vuex 仅适合维护 Vue 2 老项目。
总结
- Vuex:state → mutations(同步)→ actions,单 Store + 嵌套 modules
- Pinia:state → getters → actions(无 mutations),多 Store 独立
- 取代原因:去掉 mutations 冗余、TS/Composition API 原生支持、模块化更清晰
- 迁移策略:渐进式共存,按模块逐个迁移,mutations 逻辑合并到 actions
- 选型:新项目用 Pinia;Vue 2 老项目继续 Vuex