Vuex vs Pinia
问题
Vuex 和 Pinia 有什么区别?为什么 Pinia 成为 Vue 3 推荐的状态管理方案?
答案
Pinia 是 Vue 官方推荐的新一代状态管理库,被认为是 Vuex 5 的实现。它更简洁、更好的 TypeScript 支持、更灵活的架构。
核心对比
| 特性 | Vuex | Pinia |
|---|---|---|
| Vue 版本 | Vue 2/3 | Vue 3(有 Vue 2 版本) |
| mutations | ✅ 必需 | ❌ 移除 |
| 模块化 | 嵌套 modules | 扁平化 stores |
| TypeScript | 需要配置 | 原生支持 |
| Devtools | ✅ | ✅ |
| 代码分割 | 需要手动配置 | 自动 |
| 体积 | ~10KB | ~1KB |
代码对比
- Vuex
- Pinia
// store/index.ts
import { createStore } from 'vuex';
export default createStore({
state: {
count: 0,
user: null
},
getters: {
doubleCount(state) {
return state.count * 2;
},
isLoggedIn(state) {
return !!state.user;
}
},
mutations: {
// 必须是同步的
INCREMENT(state) {
state.count++;
},
SET_USER(state, user) {
state.user = user;
}
},
actions: {
// 可以是异步的
async login({ commit }, credentials) {
const user = await api.login(credentials);
commit('SET_USER', user);
},
increment({ commit }) {
commit('INCREMENT');
}
},
modules: {
// 嵌套模块
cart: cartModule,
products: productsModule
}
});
// 组件中使用
import { useStore } from 'vuex';
const store = useStore();
// 访问 state
store.state.count;
store.state.cart.items;
// 访问 getters
store.getters.doubleCount;
// mutation
store.commit('INCREMENT');
// action
store.dispatch('login', { username, password });
// stores/counter.ts
import { defineStore } from 'pinia';
// Options Store 风格
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++;
}
}
});
// Setup Store 风格(推荐)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
// 异步操作直接写
async function fetchCount() {
count.value = await api.getCount();
}
return { count, doubleCount, increment, fetchCount };
});
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const isLoggedIn = computed(() => !!user.value);
async function login(credentials) {
user.value = await api.login(credentials);
}
return { user, isLoggedIn, login };
});
// 组件中使用
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const counterStore = useCounterStore();
// 直接访问(响应式)
counterStore.count;
counterStore.doubleCount;
// 解构需要 storeToRefs
const { count, doubleCount } = storeToRefs(counterStore);
// 直接调用方法
counterStore.increment();
await counterStore.fetchCount();
Pinia 的优势
1. 移除 mutations
Vuex 强制区分 mutations(同步)和 actions(异步),Pinia 简化为只有 actions:
// Vuex:需要 mutation + action
mutations: {
SET_DATA(state, data) {
state.data = data;
}
},
actions: {
async fetchData({ commit }) {
const data = await api.get();
commit('SET_DATA', data);
}
}
// Pinia:直接在 action 中修改
actions: {
async fetchData() {
this.data = await api.get(); // 直接修改
}
}
2. 更好的 TypeScript 支持
// Pinia:完美的类型推断
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
async function login(credentials: LoginCredentials): Promise<void> {
user.value = await api.login(credentials);
}
return { user, login };
});
// 使用时类型自动推断
const store = useUserStore();
store.user; // User | null
store.login({ username: '', password: '' }); // 参数类型检查
3. 扁平化 store 结构
// Vuex:嵌套模块
store.state.cart.items
store.commit('cart/ADD_ITEM', item)
store.dispatch('cart/checkout')
// Pinia:独立 store,按需导入
const cartStore = useCartStore();
cartStore.items;
cartStore.addItem(item);
cartStore.checkout();
// store 之间相互调用
export const useUserStore = defineStore('user', () => {
const cartStore = useCartStore(); // 直接使用其他 store
async function logout() {
cartStore.clear(); // 调用其他 store 的方法
}
});
4. Setup Store 风格
Setup Store 与 Composition API 完全一致:
export const useCounterStore = defineStore('counter', () => {
// state = ref
const count = ref(0);
// getters = computed
const doubleCount = computed(() => count.value * 2);
// actions = function
function increment() {
count.value++;
}
// watch
watch(count, (val) => {
console.log('count changed:', val);
});
return { count, doubleCount, increment };
});
5. 自动代码分割
// Pinia store 是按需加载的
// 只有 import 时才会加载
// 页面 A
import { useCartStore } from '@/stores/cart';
// -> 加载 cart store
// 页面 B
import { useUserStore } from '@/stores/user';
// -> 只加载 user store,不加载 cart
Store 组合与复用
// stores/useAuth.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null);
const token = ref<string | null>(null);
const isLoggedIn = computed(() => !!token.value);
async function login(credentials: Credentials) {
const response = await api.login(credentials);
token.value = response.token;
user.value = response.user;
}
function logout() {
token.value = null;
user.value = null;
}
return { user, token, isLoggedIn, login, logout };
});
// stores/useCart.ts
export const useCartStore = defineStore('cart', () => {
const authStore = useAuthStore(); // 组合其他 store
const items = ref<CartItem[]>([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0)
);
async function checkout() {
if (!authStore.isLoggedIn) {
throw new Error('Please login first');
}
await api.checkout(items.value, authStore.token);
items.value = [];
}
return { items, total, checkout };
});
持久化
// 使用 pinia-plugin-persistedstate
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// store 配置持久化
export const useUserStore = defineStore('user', () => {
const token = ref('');
return { token };
}, {
persist: {
key: 'user-store',
storage: localStorage,
pick: ['token'] // 只持久化 token
}
});
$reset 和 $patch
const store = useCounterStore();
// $reset:重置到初始状态(仅 Options Store)
store.$reset();
// $patch:批量更新
store.$patch({
count: 10,
name: 'new name'
});
// $patch 函数形式
store.$patch((state) => {
state.items.push({ id: 1, name: 'new' });
state.count++;
});
// $subscribe:订阅状态变化
store.$subscribe((mutation, state) => {
console.log('state changed:', mutation.type, state);
});
常见面试问题
Q1: 为什么 Pinia 移除了 mutations?
答案:
Vuex 的 mutations 设计初衷是:
- 追踪状态变化:devtools 可以记录每次 mutation
- 保证同步:mutation 必须同步,方便调试
但实际开发中:
- 样板代码过多:每个状态变化都要写 mutation + action
- 命名冗余:
SET_USER,UPDATE_USER,CLEAR_USER... - TypeScript 不友好:类型推断困难
Pinia 的解决方案:
- 直接在 action 中修改状态:减少样板代码
- devtools 仍然可以追踪:Pinia 同样支持时间旅行调试
- 更好的 TS 支持:类型自动推断
Q2: Pinia 如何实现响应式?
答案:
Pinia 基于 Vue 3 的响应式系统:
// defineStore 内部实现(简化)
function defineStore(id, setup) {
return function useStore() {
// 检查是否已创建
if (!pinia._s.has(id)) {
// 创建响应式 store
const store = reactive({});
// 执行 setup 函数
const setupResult = setup();
// 合并到 store
Object.assign(store, setupResult);
pinia._s.set(id, store);
}
return pinia._s.get(id);
};
}
核心是使用 reactive 包装 store 对象。
Q3: 什么时候用 Pinia,什么时候用组合式函数?
答案:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 全局状态(用户、主题) | Pinia | 需要跨组件共享 |
| 页面级状态 | Composables | 页面独立,无需全局 |
| 可复用逻辑(无状态) | Composables | 纯逻辑复用 |
| 需要 devtools 调试 | Pinia | 内置支持 |
| 需要持久化 | Pinia | 插件支持 |
| SSR | Pinia | 更好的 SSR 支持 |
// 全局状态 -> Pinia
export const useUserStore = defineStore('user', () => {
const user = ref(null);
return { user };
});
// 可复用逻辑 -> Composable
export function useMouse() {
const x = ref(0);
const y = ref(0);
// 每个组件独立状态
return { x, y };
}
Q4: Pinia store 解构为什么会丢失响应式?
答案:
const store = useCounterStore();
// ❌ 解构丢失响应式
const { count } = store;
// count 是普通值,不是 ref
// ✅ 使用 storeToRefs 保持响应式
import { storeToRefs } from 'pinia';
const { count } = storeToRefs(store);
// count 是 ref
// ✅ 方法可以直接解构(不需要响应式)
const { increment } = store;
原因:Pinia store 是 reactive 对象,解构会取出原始值。storeToRefs 会将属性转换为 ref。
Q5: 如何从 Vuex 迁移到 Pinia?
答案:
// Vuex
export default createStore({
state: { count: 0 },
mutations: {
INCREMENT(state) { state.count++; }
},
actions: {
async fetchCount({ commit }) {
const count = await api.get();
commit('SET_COUNT', count);
}
}
});
// Pinia(迁移后)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
function increment() {
count.value++;
}
async function fetchCount() {
count.value = await api.get();
}
return { count, increment, fetchCount };
});
迁移步骤:
- 安装 Pinia
- 将
state改为ref/reactive - 将
getters改为computed - 将
mutations+actions合并为函数 - 更新组件中的使用方式