跳到主要内容

Vuex vs Pinia

问题

Vuex 和 Pinia 有什么区别?为什么 Pinia 成为 Vue 3 推荐的状态管理方案?

答案

Pinia 是 Vue 官方推荐的新一代状态管理库,被认为是 Vuex 5 的实现。它更简洁、更好的 TypeScript 支持、更灵活的架构。

核心对比

特性VuexPinia
Vue 版本Vue 2/3Vue 3(有 Vue 2 版本)
mutations必需移除
模块化嵌套 modules扁平化 stores
TypeScript需要配置原生支持
Devtools
代码分割需要手动配置自动
体积~10KB~1KB

代码对比

// 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 });

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 设计初衷是:

  1. 追踪状态变化:devtools 可以记录每次 mutation
  2. 保证同步:mutation 必须同步,方便调试

但实际开发中:

  1. 样板代码过多:每个状态变化都要写 mutation + action
  2. 命名冗余SET_USER, UPDATE_USER, CLEAR_USER...
  3. 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插件支持
SSRPinia更好的 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 };
});

迁移步骤:

  1. 安装 Pinia
  2. state 改为 ref/reactive
  3. getters 改为 computed
  4. mutations + actions 合并为函数
  5. 更新组件中的使用方式

相关链接