在 Vue 生态中,状态管理一直是构建复杂应用的关键环节。Vuex 作为官方状态管理库统治了多年,而 Pinia 作为新秀正迅速崛起。本文将从多个维度深度对比这两者,帮助你做出合适的技术选型。

背景介绍

Vuex:成熟稳重的老将

Vuex 是 Vue.js 的官方状态管理库,采用集中式存储管理应用的所有组件的状态。它基于 Flux 架构,提供了严格的状态变更流程。

javascript

// Vuex 示例
import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

Pinia:轻量灵活的新星

Pinia 是 Vue.js 的下一代状态管理库,由 Vue 核心团队维护。它提供了更简洁的 API,同时完美支持 Composition API。

javascript

// Pinia 示例
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      setTimeout(() => {
        this.increment()
      }, 1000)
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

核心架构对比

1. 设计理念差异

Vuex:

  • 基于 Flux 架构的严格单向数据流

  • 强调可预测的状态变更

  • 适合大型复杂应用

Pinia:

  • 更灵活直观的设计

  • 减少模板代码

  • 拥抱 Composition API 理念

2. 状态变更流程

Vuex 的严格流程:

text

Component → Dispatch Action → Commit Mutation → Update State → Re-render Component

Pinia 的灵活流程:

text

Component → Direct Action Call → Update State → Re-render Component
    或
Component → Direct State Mutation → Update State → Re-render Component

API 设计对比

1. 创建 Store

Vuex:

javascript

const store = createStore({
  state: { 
    user: null,
    tokens: {}
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user
    }
  },
  actions: {
    async login({ commit }, credentials) {
      const user = await api.login(credentials)
      commit('SET_USER', user)
    }
  },
  getters: {
    isAuthenticated: state => !!state.user
  }
})

Pinia:

javascript

export const useAuthStore = defineStore('auth', {
  state: () => ({ 
    user: null,
    tokens: {}
  }),
  actions: {
    async login(credentials) {
      this.user = await api.login(credentials)
    }
  },
  getters: {
    isAuthenticated: (state) => !!state.user
  }
})

// 或使用组合式 API 风格
export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const tokens = ref({})
  
  const isAuthenticated = computed(() => !!user.value)
  
  async function login(credentials) {
    user.value = await api.login(credentials)
  }
  
  return { user, tokens, isAuthenticated, login }
})

2. 在组件中使用

Vuex:

vue

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementAsync">Async +1</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },
  methods: {
    ...mapActions(['increment', 'incrementAsync'])
  }
}
</script>

<!-- 组合式 API 版本 -->
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const count = computed(() => store.state.count)
const doubleCount = computed(() => store.getters.doubleCount)

const increment = () => store.dispatch('increment')
const incrementAsync = () => store.dispatch('incrementAsync')
</script>

Pinia:

vue

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementAsync">Async +1</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// 保持响应式的解构
const { count, doubleCount } = storeToRefs(counterStore)
const { increment, incrementAsync } = counterStore
</script>

TypeScript 支持对比

Vuex 的 TypeScript 支持

Vuex 需要额外的类型定义来获得完整的 TypeScript 支持:

typescript

// vuex-shim.d.ts
import { Store } from 'vuex'

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: Store<State>
  }
}

interface State {
  count: number
  user: User | null
}

// 创建 store 时的类型定义
const store = createStore<State>({
  state: {
    count: 0,
    user: null
  },
  // ... 其他配置
})

Pinia 的 TypeScript 支持

Pinia 提供开箱即用的完整 TypeScript 支持:

typescript

import { defineStore } from 'pinia'

interface User {
  id: number
  name: string
  email: string
}

interface AuthState {
  user: User | null
  tokens: {
    accessToken: string
    refreshToken: string
  }
}

export const useAuthStore = defineStore('auth', {
  state: (): AuthState => ({
    user: null,
    tokens: {
      accessToken: '',
      refreshToken: ''
    }
  }),
  
  getters: {
    isAuthenticated: (state): boolean => !!state.user,
    userName: (state): string => state.user?.name || ''
  },
  
  actions: {
    async login(credentials: { email: string; password: string }): Promise<void> {
      this.user = await api.login(credentials)
    },
    
    logout(): void {
      this.user = null
      this.tokens = { accessToken: '', refreshToken: '' }
    }
  }
})

模块化对比

Vuex 的模块系统

javascript

// modules/user.js
const userModule = {
  namespaced: true,
  state: () => ({
    profile: null,
    preferences: {}
  }),
  mutations: {
    SET_PROFILE(state, profile) {
      state.profile = profile
    }
  },
  actions: {
    fetchProfile({ commit }) {
      // 获取用户资料
    }
  }
}

// store/index.js
import { createStore } from 'vuex'
import user from './modules/user'
import products from './modules/products'

export default createStore({
  modules: {
    user,
    products
  }
})

// 在组件中使用(需要命名空间)
export default {
  computed: {
    ...mapState('user', ['profile']),
    ...mapGetters('user', ['isPremiumUser'])
  },
  methods: {
    ...mapActions('user', ['fetchProfile'])
  }
}

Pinia 的模块化

Pinia 采用天然的模块化设计,每个 store 都是独立的:

javascript

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null,
    preferences: {}
  }),
  actions: {
    async fetchProfile() {
      this.profile = await api.getProfile()
    }
  }
})

// stores/products.js
export const useProductsStore = defineStore('products', {
  state: () => ({
    items: [],
    categories: []
  })
})

// 在组件中使用多个 store
import { useUserStore } from '@/stores/user'
import { useProductsStore } from '@/stores/products'

const userStore = useUserStore()
const productsStore = useProductsStore()

性能与体积对比

包大小

  • Vuex: ~10KB (gzipped)

  • Pinia: ~5KB (gzipped) - 体积减少约 50%

运行时性能

两者在运行时性能上差异不大,但 Pinia 由于设计更简洁,在大型应用中可能有轻微优势。

开发体验对比

学习曲线

  • Vuex: 中等,需要理解 Mutation、Action 的概念和区别

  • Pinia: 简单,API 直观,符合直觉

代码简洁性

  • Vuex: 需要更多的模板代码

  • Pinia: 代码更简洁,开发效率更高

DevTools 支持

两者都支持 Vue DevTools,但 Pinia 在 DevTools 中提供了更清晰的展示。

迁移指南

从 Vuex 迁移到 Pinia

  1. 安装 Pinia

bash

npm install pinia
  1. 更新 main.js

javascript

// 之前
import { createApp } from 'vue'
import { createStore } from 'vuex'

const store = createStore({ /* 配置 */ })

const app = createApp(App)
app.use(store)

// 之后
import { createApp } from 'vue'
import { createPinia } from 'pinia'

const pinia = createPinia()

const app = createApp(App)
app.use(pinia)
  1. 转换 Store

javascript

// Vuex
const store = createStore({
  state: { count: 0 },
  mutations: {
    increment(state) { state.count++ }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

// 转换为 Pinia
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() { this.count++ },
    async incrementAsync() {
      setTimeout(() => this.increment(), 1000)
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

总结与建议

选择 Vuex 的情况:

  • 维护现有的 Vuex 项目

  • 团队已经熟悉 Vuex 且没有迁移需求

  • 需要严格的单向数据流约束

  • 项目依赖 Vuex 特定的插件或生态

选择 Pinia 的情况:

  • 启动新项目

  • 希望更好的 TypeScript 支持

  • 追求更简洁的 API 和开发体验

  • 使用 Composition API 为主的项目

  • 需要更小的包体积

功能对比表格

特性 Vuex Pinia
Vue 2 支持 ✅ (需要 Composition API)
Vue 3 支持
TypeScript 支持 🔶 (需要配置) ✅ (开箱即用)
包大小 ~10KB ~5KB
学习曲线 中等 简单
Composition API 🔶
Options API
DevTools
模块热更新
服务端渲染

未来展望

Vuex 目前处于维护模式,Vue 官方推荐在新项目中使用 Pinia。Pinia 已经成为 Vue 3 默认的状态管理解决方案,并且会随着 Vue 生态继续发展。

结论

Pinia 在设计上更加现代化,提供了更好的开发体验和 TypeScript 支持。对于新项目,强烈推荐使用 Pinia。对于现有使用 Vuex 的项目,如果没有遇到具体问题,可以继续使用 Vuex,但在考虑重构或升级时可以逐步迁移到 Pinia。

无论选择哪个,良好的状态管理设计都比工具本身更重要。希望这篇对比能帮助你做出合适的技术决策!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐