Vuex 与 Pinia 对比指南
本文总结 Vuex 和 Pinia 的核心区别、各自完整用法、以及模块化(modules)写法,供开发参考。
一、核心区别
| Vuex | Pinia | |
|---|---|---|
| 适用 | Vue 2(也支持 Vue 3,但已进入维护模式) | Vue 3(官方推荐) |
| 修改数据 | mutations(同步)+ actions(异步)必须分开 |
只有 actions,直接改,无 mutations |
| 模块化 | 手动 module + namespaced: true |
天然模块化,无需配置 |
| TypeScript | 支持差,类型要额外处理 | 原生支持,类型推断好 |
| 使用方式 | this.$store + mapState/mapGetters/mapActions/mapMutations |
useXxxStore() 直接调用 |
| 体积 | 较大 | 更轻量 |
二、Vuex 使用方法
1. 安装
npm install vuex # Vue 2 用 vuex@3,Vue 3 用 vuex@4
2. 创建 store
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: { // 数据
count: 0,
userInfo: {}
},
mutations: { // 同步修改(唯一能改 state 的地方)
INCREMENT(state) { state.count++ },
SET_USER(state, user) { state.userInfo = user }
},
actions: { // 异步操作,内部再 commit
asyncIncrement({ commit }) {
setTimeout(() => commit('INCREMENT'), 1000)
}
},
getters: { // 计算属性
doubleCount: state => state.count * 2
}
})
3. 注册
// main.js
import store from './store'
new Vue({ store, render: h => h(App) }).$mount('#app')
4. 组件里使用
// 直接访问
this.$store.state.count
this.$store.commit('INCREMENT') // 同步修改
this.$store.dispatch('asyncIncrement') // 异步操作
this.$store.getters.doubleCount
// 或用辅助函数(配合 computed / methods)
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
}
methods: {
...mapActions(['asyncIncrement']),
...mapMutations(['INCREMENT'])
}
三、Pinia 使用方法
1. 安装
npm install pinia
2. 创建 store
Setup 风格(推荐,类似组合式 API):
// store/user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const setToken = (val: string) => { token.value = val }
return { token, setToken }
})
Options 风格(类似 Vuex,带 state/getters/actions):
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() { this.count++ }
}
})
3. 注册
// main.ts
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
4. 组件里使用
<script setup lang="ts">
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
userStore.setToken('abc') // 调用 action
console.log(userStore.token) // 读 state
console.log(userStore.doubleCount) // 读 getter
</script>
四、模块化(modules)
项目变大后,按功能拆分模块,每个模块管自己的数据。
Vuex 的模块化(复杂,要配 namespaced)
目录结构:
store/
├── index.js # 根 store,组装所有模块
└── modules/
├── user.js # 用户模块
└── cart.js # 购物车模块
写一个模块 store/modules/user.js:
export default {
namespaced: true, // ⚠️ 必须写,否则 mutations/actions 会全局冲突
state: {
token: '',
userInfo: null
},
mutations: {
SET_TOKEN(state, token) {
state.token = token
}
},
actions: {
async login({ commit }, form) {
const token = await api.login(form)
commit('SET_TOKEN', token)
}
},
getters: {
isLogin: state => !!state.token
}
}
根 store 组装 store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user, // 注册模块
cart
}
})
组件里访问(带模块前缀):
// 直接访问:要加模块名前缀
this.$store.state.user.token // state
this.$store.commit('user/SET_TOKEN', 'x') // mutation 要 user/ 前缀
this.$store.dispatch('user/login', form) // action 要 user/ 前缀
this.$store.getters['user/isLogin'] // getter 要 user/ 前缀
// 辅助函数:第一个参数传模块名
import { mapState, mapActions } from 'vuex'
computed: { ...mapState('user', ['token', 'userInfo']) }
methods: { ...mapActions('user', ['login']) }
Pinia 的模块化(简单,天然隔离)
目录结构:
store/
├── index.ts # (可选)创建 pinia 实例
└── modules/
├── user.ts # 用户模块
└── cart.ts # 购物车模块
写一个模块 store/modules/user.ts:
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
const setToken = (val: string) => { token.value = val }
const login = async (form: any) => {
const res = await api.login(form)
setToken(res.token)
}
return { token, userInfo, setToken, login }
})
每个
defineStore('名字', ...)的「名字」就是模块名,天然隔离,不需要namespaced。
组件里访问(直接 use 对应 store):
import { useUserStore } from '@/store/modules/user'
import { useCartStore } from '@/store/modules/cart'
const userStore = useUserStore() // 拿到用户模块
const cartStore = useCartStore() // 拿到购物车模块
userStore.token // 读,没有前缀
userStore.setToken('x') // 调 action,没有前缀
userStore.login(form)
模块间互相调用对比
Vuex(要带命名空间前缀 + root):
// user 模块里调用 cart 模块的 action
dispatch('cart/addItem', item, { root: true })
Pinia(直接 use 另一个 store):
// user 模块里调用 cart 模块
const cartStore = useCartStore()
cartStore.addItem(item)
五、模块化对比总结
| Vuex | Pinia | |
|---|---|---|
| 模块怎么定义 | modules: { user, cart } 手动注册 |
defineStore('user', ...) 定义即模块 |
| 命名空间 | 必须写 namespaced: true,否则冲突 |
无此概念,天然隔离 |
| 访问前缀 | this.$store.state.user.xxx、user/xxx |
直接 userStore.xxx |
| 一个模块一个文件 | ✅ 是 | ✅ 是 |
| 模块间互相调用 | dispatch('other/xxx', data, { root: true }) |
useOtherStore().xxx() |
六、最直观的写法对比
| 操作 | Vuex | Pinia |
|---|---|---|
| 同步改数据 | commit('INCREMENT') |
store.increment() 直接调 |
| 异步改数据 | dispatch('asyncIncrement') |
store.asyncIncrement()(同一个方法) |
| 读数据 | this.$store.state.count |
store.count |
| 计算属性 | getters + mapGetters |
getters 或直接 computed |
七、选型建议
- Vue 3 新项目:直接用 Pinia,不用犹豫。
- Vue 2 老项目:继续用 Vuex,没必要迁移。
概念对应关系:state↔state、getters↔getters、actions↔actions,唯一消失的是 Vuex 的 mutations(Pinia 合并进了 actions)。
八、一句话记忆
Pinia 就是「删掉 mutations、去掉 module/namespaced 复杂度、TS 更好」的 Vuex 简化版。
模块化上,Pinia 是「每个 store 一个文件,用时直接 import 对应的useXxxStore」,没有 Vuex 那一堆命名空间的概念。
浙公网安备 33010602011771号