Vue 状态管理
2026/3/20大约 6 分钟
Vue 状态管理:从 Vuex 到 Pinia 的最佳实践
当应用变得复杂,组件间共享状态成为一个挑战。Vue 生态提供了两个官方状态管理方案:Vuex(Vue2/Vue3)和 Pinia(Vue3 推荐)。本文将全面介绍这两个工具的使用方法和最佳实践。
为什么需要状态管理?
问题场景
组件树:
App
/ \
NavBar Main
/ \
UserInfo Content
|
Article
|
Comment
问题:UserInfo 和 Comment 都需要用户信息
方案1:逐层传递 props(繁琐)
方案2:EventBus(难以追踪)
方案3:状态管理(推荐)
状态管理的核心概念
// 单向数据流
View → Actions → State → View
// 状态管理三要素
State // 数据源
Getters // 派生状态(computed)
Actions // 修改状态的方法
Vuex(Vue2/Vue3)
安装配置
# Vue2
npm install vuex@3
# Vue3
npm install vuex@4
// Vue2 + Vuex3
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {}
})
export default store
// Vue3 + Vuex4
import { createStore } from 'vuex'
const store = createStore({
state() {
return {}
},
getters: {},
mutations: {},
actions: {},
modules: {}
})
export default store
State
const store = createStore({
state() {
return {
count: 0,
user: null,
todos: []
}
}
})
// 组件中访问
// Vue2 Options API
computed: {
count() {
return this.$store.state.count
}
}
// 使用 mapState 辅助函数
import { mapState } from 'vuex'
computed: {
...mapState(['count', 'user']),
...mapState({
todoCount: state => state.todos.length
})
}
// Vue3 Composition API
import { useStore } from 'vuex'
import { computed } from 'vue'
const store = useStore()
const count = computed(() => store.state.count)
Getters
const store = createStore({
state() {
return {
todos: [
{ id: 1, text: '学习 Vue', done: true },
{ id: 2, text: '学习 Vuex', done: false }
]
}
},
getters: {
// 基础 getter
doneTodos(state) {
return state.todos.filter(todo => todo.done)
},
// 访问其他 getter
doneTodosCount(state, getters) {
return getters.doneTodos.length
},
// 返回函数(带参数的 getter)
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})
// 组件中访问
computed: {
doneTodos() {
return this.$store.getters.doneTodos
}
}
// 使用 mapGetters
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['doneTodos', 'doneTodosCount']),
...mapGetters({
done: 'doneTodos'
})
}
// 带参数的 getter
this.$store.getters.getTodoById(1)
Mutations
Mutations 是唯一修改 state 的方式,必须是同步函数。
const store = createStore({
state() {
return {
count: 0,
user: null
}
},
mutations: {
// 简单 mutation
increment(state) {
state.count++
},
// 带 payload
incrementBy(state, payload) {
state.count += payload.amount
},
// 设置用户
SET_USER(state, user) {
state.user = user
},
// 重置状态
RESET_STATE(state) {
Object.assign(state, getDefaultState())
}
}
})
// 提交 mutation
this.$store.commit('increment')
this.$store.commit('incrementBy', { amount: 10 })
// 对象风格
this.$store.commit({
type: 'incrementBy',
amount: 10
})
// 使用 mapMutations
import { mapMutations } from 'vuex'
methods: {
...mapMutations(['increment']),
...mapMutations({
add: 'increment'
})
}
Actions
Actions 用于处理异步操作,通过提交 mutation 来修改 state。
const store = createStore({
state() {
return {
user: null,
loading: false
}
},
mutations: {
SET_USER(state, user) {
state.user = user
},
SET_LOADING(state, loading) {
state.loading = loading
}
},
actions: {
// 基础 action
async fetchUser({ commit, state }, userId) {
commit('SET_LOADING', true)
try {
const user = await api.getUser(userId)
commit('SET_USER', user)
return user
} finally {
commit('SET_LOADING', false)
}
},
// 组合 action
async login({ commit, dispatch }, credentials) {
const { token, userId } = await api.login(credentials)
localStorage.setItem('token', token)
await dispatch('fetchUser', userId)
},
// 访问 rootState(模块中)
someAction({ state, commit, rootState, rootGetters }) {
// ...
}
}
})
// 分发 action
this.$store.dispatch('fetchUser', 123)
// 使用 mapActions
import { mapActions } from 'vuex'
methods: {
...mapActions(['fetchUser', 'login']),
...mapActions({
getUser: 'fetchUser'
})
}
Modules
// store/modules/user.js
export default {
namespaced: true,
state() {
return {
profile: null,
permissions: []
}
},
getters: {
isAdmin(state) {
return state.permissions.includes('admin')
}
},
mutations: {
SET_PROFILE(state, profile) {
state.profile = profile
}
},
actions: {
async fetchProfile({ commit }) {
const profile = await api.getProfile()
commit('SET_PROFILE', profile)
}
}
}
// store/modules/cart.js
export default {
namespaced: true,
state() {
return {
items: []
}
},
getters: {
totalPrice(state) {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
mutations: {
ADD_ITEM(state, item) {
state.items.push(item)
}
},
actions: {
addItem({ commit }, item) {
commit('ADD_ITEM', item)
}
}
}
// store/index.js
import { createStore } from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
export default createStore({
modules: {
user,
cart
}
})
// 访问模块
// state
this.$store.state.user.profile
this.$store.state.cart.items
// getters
this.$store.getters['user/isAdmin']
this.$store.getters['cart/totalPrice']
// mutations
this.$store.commit('user/SET_PROFILE', profile)
// actions
this.$store.dispatch('user/fetchProfile')
// 使用 mapState
...mapState('user', ['profile', 'permissions'])
...mapGetters('user', ['isAdmin'])
...mapMutations('user', ['SET_PROFILE'])
...mapActions('user', ['fetchProfile'])
Pinia(Vue3 推荐)
Pinia 是 Vue 官方推荐的新一代状态管理库,具有更简洁的 API 和更好的 TypeScript 支持。
安装配置
npm install pinia
// main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
定义 Store
// stores/counter.js
import { defineStore } from "pinia";
// Option Store(类似 Vuex)
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
name: "Counter",
}),
getters: {
doubleCount: (state) => state.count * 2,
// 使用 this 访问其他 getter
doubleCountPlusOne() {
return this.doubleCount + 1;
},
},
actions: {
increment() {
this.count++;
},
async fetchCount() {
const count = await api.getCount();
this.count = count;
},
},
});
// Setup Store(使用 Composition API)
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const name = ref("Counter");
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
async function fetchCount() {
count.value = await api.getCount();
}
return {
count,
name,
doubleCount,
increment,
fetchCount,
};
});
使用 Store
<script setup>
import { useCounterStore } from "@/stores/counter";
import { storeToRefs } from "pinia";
const counterStore = useCounterStore();
// 直接访问
console.log(counterStore.count);
console.log(counterStore.doubleCount);
// 解构(使用 storeToRefs 保持响应性)
const { count, name } = storeToRefs(counterStore);
// 方法可以直接解构
const { increment, fetchCount } = counterStore;
// 修改 state
counterStore.count++;
counterStore.increment();
// 批量修改
counterStore.$patch({
count: counterStore.count + 1,
name: "New Name",
});
// 使用函数
counterStore.$patch((state) => {
state.count++;
state.name = "New Name";
});
// 替换整个 state
counterStore.$state = { count: 0, name: "Reset" };
// 重置 state
counterStore.$reset();
// 订阅变化
counterStore.$subscribe((mutation, state) => {
console.log("State changed:", mutation, state);
});
// 订阅 actions
counterStore.$onAction(
({
name, // action 名称
store, // store 实例
args, // 参数
after, // action 完成后的钩子
onError, // 错误钩子
}) => {
console.log(`Action ${name} called with`, args);
after((result) => {
console.log(`Action ${name} returned`, result);
});
onError((error) => {
console.error(`Action ${name} failed`, error);
});
}
);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ counterStore.doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
完整示例
// stores/user.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import * as api from "@/api/user";
export const useUserStore = defineStore("user", () => {
// state
const user = ref(null);
const token = ref(localStorage.getItem("token") || "");
const loading = ref(false);
// getters
const isLoggedIn = computed(() => !!token.value);
const username = computed(() => user.value?.name || "游客");
const isAdmin = computed(() => user.value?.role === "admin");
// actions
async function login(credentials) {
loading.value = true;
try {
const res = await api.login(credentials);
token.value = res.token;
localStorage.setItem("token", res.token);
await fetchUser();
} finally {
loading.value = false;
}
}
async function fetchUser() {
if (!token.value) return;
user.value = await api.getUser();
}
function logout() {
user.value = null;
token.value = "";
localStorage.removeItem("token");
}
return {
user,
token,
loading,
isLoggedIn,
username,
isAdmin,
login,
fetchUser,
logout,
};
});
// stores/cart.js
import { defineStore } from "pinia";
import { useUserStore } from "./user";
export const useCartStore = defineStore("cart", {
state: () => ({
items: [],
}),
getters: {
totalItems: (state) => state.items.length,
totalPrice: (state) => {
return state.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0);
},
// 访问其他 store
itemsWithDiscount() {
const userStore = useUserStore();
const discount = userStore.isVIP ? 0.9 : 1;
return this.items.map((item) => ({
...item,
finalPrice: item.price * discount,
}));
},
},
actions: {
addItem(product) {
const existing = this.items.find((item) => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
this.items.push({ ...product, quantity: 1 });
}
},
removeItem(productId) {
const index = this.items.findIndex((item) => item.id === productId);
if (index > -1) {
this.items.splice(index, 1);
}
},
clearCart() {
this.items = [];
},
async checkout() {
const userStore = useUserStore();
if (!userStore.isLoggedIn) {
throw new Error("请先登录");
}
await api.createOrder(this.items);
this.clearCart();
},
},
});
TypeScript 支持
// stores/user.ts
import { defineStore } from "pinia";
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
interface UserState {
user: User | null;
token: string;
loading: boolean;
}
export const useUserStore = defineStore("user", {
state: (): UserState => ({
user: null,
token: "",
loading: false,
}),
getters: {
isLoggedIn(): boolean {
return !!this.token;
},
isAdmin(): boolean {
return this.user?.role === "admin";
},
},
actions: {
async login(email: string, password: string): Promise<void> {
this.loading = true;
try {
const res = await api.login({ email, password });
this.token = res.token;
await this.fetchUser();
} finally {
this.loading = false;
}
},
async fetchUser(): Promise<User | null> {
if (!this.token) return null;
this.user = await api.getUser();
return this.user;
},
},
});
持久化插件
// 使用 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store 中启用
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
preferences: {}
}),
persist: true, // 启用持久化
// 或详细配置
persist: {
key: 'user-store',
storage: localStorage,
paths: ['token'] // 只持久化特定字段
}
})
Vuex vs Pinia 对比
| 特性 | Vuex | Pinia |
|---|---|---|
| Vue 版本 | Vue2/Vue3 | Vue3(Vue2 需要插件) |
| TypeScript | 需要额外配置 | 原生支持 |
| API 风格 | mutations + actions | 只有 actions |
| 模块 | 需要 namespaced | 自动命名空间 |
| 代码体积 | 较大 | 更小 |
| 学习曲线 | 较陡 | 更平缓 |
| DevTools | 支持 | 支持 |
最佳实践
1. 合理划分 Store
// ❌ 一个巨大的 store
const store = defineStore("app", {
state: () => ({
user: null,
cart: [],
products: [],
orders: [],
settings: {},
}),
});
// ✅ 按功能划分
const useUserStore = defineStore("user", {
/* ... */
});
const useCartStore = defineStore("cart", {
/* ... */
});
const useProductStore = defineStore("product", {
/* ... */
});
const useOrderStore = defineStore("order", {
/* ... */
});
const useSettingsStore = defineStore("settings", {
/* ... */
});
2. 避免直接修改深层对象
// ❌ 直接修改
store.user.profile.settings.theme = 'dark'
// ✅ 使用 action
actions: {
updateUserTheme(theme) {
this.user = {
...this.user,
profile: {
...this.user.profile,
settings: {
...this.user.profile.settings,
theme
}
}
}
}
}
3. 组合多个 Store
// stores/checkout.js
import { defineStore } from "pinia";
import { useCartStore } from "./cart";
import { useUserStore } from "./user";
export const useCheckoutStore = defineStore("checkout", () => {
const cartStore = useCartStore();
const userStore = useUserStore();
async function processCheckout() {
if (!userStore.isLoggedIn) {
throw new Error("请先登录");
}
if (cartStore.items.length === 0) {
throw new Error("购物车为空");
}
// 处理结账逻辑
await api.checkout({
userId: userStore.user.id,
items: cartStore.items,
});
cartStore.clearCart();
}
return { processCheckout };
});
总结
状态管理是大型 Vue 应用的重要组成部分:
Vuex:
- 成熟稳定,适合 Vue2 项目
- 严格的单向数据流(mutations)
- 完善的 DevTools 支持
Pinia:
- Vue3 官方推荐
- 更简洁的 API(无 mutations)
- 更好的 TypeScript 支持
- 更小的体积
选择建议:
- 新的 Vue3 项目:使用 Pinia
- 已有 Vue2 项目:继续使用 Vuex
- Vue2 升级 Vue3:考虑迁移到 Pinia
掌握状态管理,让你的 Vue 应用更加健壮和可维护。