Vuex 与 Pinia 状态管理

Vue 状态管理库的使用与原理对比

问题

解释 Vuex 和 Pinia 的状态管理原理,包括 State、Getter、Mutation、Action 的作用和使用方式。

解答

Vuex 基本概念

Vuex 是 Vue 2/3 的集中式状态管理库,采用单向数据流。

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

const store = createStore({
  // State: 存储应用状态
  state() {
    return {
      count: 0,
      todos: []
    }
  },

  // Getters: 计算派生状态(类似计算属性)
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
    completedTodos(state) {
      return state.todos.filter(todo => todo.done)
    },
    // 可以访问其他 getter
    completedCount(state, getters) {
      return getters.completedTodos.length
    }
  },

  // Mutations: 同步修改状态的唯一方式
  mutations: {
    increment(state) {
      state.count++
    },
    addTodo(state, todo) {
      state.todos.push(todo)
    },
    setTodos(state, todos) {
      state.todos = todos
    }
  },

  // Actions: 处理异步操作,提交 mutation
  actions: {
    async fetchTodos({ commit }) {
      const res = await fetch('/api/todos')
      const todos = await res.json()
      commit('setTodos', todos)
    },
    // action 可以调用其他 action
    async addTodoAsync({ commit, dispatch }, todo) {
      await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(todo)
      })
      commit('addTodo', todo)
    }
  }
})

export default store
<!-- 组件中使用 -->
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="fetchTodos">加载</button>
  </div>
</template>

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

export default {
  computed: {
    // 映射 state
    ...mapState(['count', 'todos']),
    // 映射 getters
    ...mapGetters(['doubleCount', 'completedTodos'])
  },
  methods: {
    // 映射 mutations
    ...mapMutations(['increment', 'addTodo']),
    // 映射 actions
    ...mapActions(['fetchTodos'])
  }
}
</script>

Pinia 基本概念

Pinia 是 Vue 3 推荐的状态管理库,API 更简洁,移除了 Mutation。

// stores/counter.js
import { defineStore } from 'pinia'

// Option Store 写法
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    todos: []
  }),

  getters: {
    doubleCount: (state) => state.count * 2,
    completedTodos: (state) => state.todos.filter(t => t.done)
  },

  actions: {
    // 直接修改状态,无需 mutation
    increment() {
      this.count++
    },
    async fetchTodos() {
      const res = await fetch('/api/todos')
      this.todos = await res.json()
    }
  }
})

// Setup Store 写法(更灵活)
export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const todos = ref([])

  // getters
  const doubleCount = computed(() => count.value * 2)

  // actions
  function increment() {
    count.value++
  }

  async function fetchTodos() {
    const res = await fetch('/api/todos')
    todos.value = await res.json()
  }

  return { count, todos, doubleCount, increment, fetchTodos }
})
<!-- 组件中使用 -->
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment()">+1</button>
  </div>
</template>

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

const counter = useCounterStore()

// 解构时保持响应式
const { count, doubleCount } = storeToRefs(counter)
// actions 可以直接解构
const { increment } = counter
</script>

响应式原理

// 简化版 Pinia 实现原理
function defineStore(id, options) {
  return function useStore() {
    // 单例模式:同一个 store 只创建一次
    if (!storeMap.has(id)) {
      const store = createStore(id, options)
      storeMap.set(id, store)
    }
    return storeMap.get(id)
  }
}

function createStore(id, options) {
  // 使用 reactive 创建响应式 state
  const state = reactive(options.state())

  // getters 转换为 computed
  const getters = {}
  Object.keys(options.getters || {}).forEach(key => {
    getters[key] = computed(() => options.getters[key](state))
  })

  // actions 绑定 this 到 store
  const actions = {}
  Object.keys(options.actions || {}).forEach(key => {
    actions[key] = options.actions[key].bind({ ...state, ...getters })
  })

  return reactive({
    ...state,
    ...getters,
    ...actions
  })
}

Vuex vs Pinia 对比

特性VuexPinia
Mutation必须通过 mutation 修改无,直接在 action 中修改
TypeScript支持较弱完整类型推断
模块化需要 modules 配置天然多 store
体积~10KB~1KB
DevTools支持支持
Vue 版本2 & 33(有 Vue 2 插件)

关键点

  • State: 响应式数据源,通过 reactive() 实现
  • Getter: 派生状态,基于 computed() 实现缓存
  • Mutation (Vuex): 同步修改状态的唯一方式,便于 DevTools 追踪
  • Action: 处理异步逻辑,Pinia 中可直接修改状态
  • Pinia 优势: 无 mutation、更好的 TS 支持、更小体积、更简洁 API