Skip to content

Vuex 状态管理

大白话解释: Vuex 就像一个"公共记账本"。多个组件需要共享数据时,不用传来传去,都去记账本上看和改。

为什么需要 Vuex?

  • 多个组件共享数据:用户登录信息、主题设置、购物车等,很多组件都需要用
  • 避免层层传递:不用从爷爷组件传到爸爸组件再传到孙子组件
  • 数据变化可追踪:所有修改都记录在案,方便调试

Vuex 的核心概念:

  • State:记账本上的数据(存储状态)
  • Getters:根据数据算出来的值(比如购物车总价)
  • Mutation:修改数据的唯一方式(必须同步,像记账)
  • Action:处理异步操作(如发请求),然后调用 Mutation 修改数据

Vue 2 官方的状态管理库,采用集中式存储管理应用的所有组件的状态。核心理念:State → Mutations → Actions 的单向数据流。


核心概念

┌─────────────────────────────────────────┐
│                 Vuex Store               │
│                                         │
│  State ──→ Getters(派生状态)            │
│    ↑                                     │
│  Mutations(同步修改) ←── Actions(异步) │
│    ↑                      ↑              │
│    └──── commit ──────────┘              │
│                                         │
└─────────────────────────────────────────┘
         ↑                    ↑
      dispatch             commit
         ↑                    ↑
     Components           Components

安装与基本配置

bash
yarn add vuex@3  # Vue 2 用 Vuex 3

# ⚠️ Vue 3 需要使用 Vuex 4:
# yarn add vuex@4
# Vuex 4 API 与 3 基本一致,但安装方式改为 createStore()
js
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

// 注册 Vuex 插件,使所有组件可通过 this.$store 访问
Vue.use(Vuex)

const store = new Vuex.Store({
  // 严格模式 —— 开发环境开启,生产环境关闭
  // ⚠️ 严格模式下不允许直接修改 state,只能通过 mutation
  strict: process.env.NODE_ENV !== 'production',

  // 状态 —— 应用的单一状态树,所有共享数据存放于此
  state: {
    count: 0,
    user: null,
    token: '',
    theme: 'light',
    todos: [],
  },

  // 计算属性(派生状态)—— 基于 state 计算得出,有缓存
  getters: {
    doubleCount: (state) => state.count * 2,
    isLoggedIn: (state) => !!state.token,
    userName: (state) => state.user?.name || '未登录',
    userRoles: (state) => state.user?.roles || [],
    isAdmin: (state, getters) => getters.userRoles.includes('admin'),
    activeTodos: (state) => state.todos.filter((t) => !t.done),
    doneTodos: (state) => state.todos.filter((t) => t.done),
    // getter 返回函数(带参数)—— 注意:返回函数时没有缓存
    todoById: (state) => (id) => state.todos.find((t) => t.id === id),
  },

  // 同步修改 —— 唯一能修改 state 的方式,必须是同步函数
  mutations: {
    SET_COUNT(state, value) {
      state.count = value
    },
    INCREMENT(state) {
      state.count++
    },
    SET_USER(state, user) {
      state.user = user
    },
    SET_TOKEN(state, token) {
      state.token = token
    },
    SET_THEME(state, theme) {
      state.theme = theme
    },
    ADD_TODO(state, todo) {
      state.todos.push(todo)
    },
    TOGGLE_TODO(state, id) {
      const todo = state.todos.find((t) => t.id === id)
      if (todo) todo.done = !todo.done
    },
    REMOVE_TODO(state, id) {
      state.todos = state.todos.filter((t) => t.id !== id)
    },
  },

  // 异步操作 —— 处理异步逻辑后通过 commit 调用 mutation
  actions: {
    async login({ commit }, credentials) {
      // commit 用于触发 mutation
      try {
        // 假设已引入:import api from '@/api/user'
        const res = await api.login(credentials)
        commit('SET_TOKEN', res.data.token)
        commit('SET_USER', res.data.user)
        return res.data
      } catch (err) {
        throw err
      }
    },

    async logout({ commit }) {
      await api.logout()
      commit('SET_TOKEN', '')
      commit('SET_USER', null)
    },

    async fetchTodos({ commit }) {
      const res = await api.getTodos()
      res.data.forEach((todo) => commit('ADD_TODO', todo))
    },

    async fetchUser({ commit }) {
      const res = await api.getUserInfo()
      commit('SET_USER', res.data)
    },

    // 组合多个 action —— dispatch 用于触发其他 action
    async initApp({ dispatch }) {
      await dispatch('fetchUser')
      await dispatch('fetchTodos')
    },
  },

  // 模块化 —— 将 store 拆分为多个模块
  modules: {},
})

export default store
js
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({
  store,
  render: (h) => h(App),
}).$mount('#app')

在组件中使用

基本读取与修改

vue
<script>
export default {
  computed: {
    // 读取 state
    count() { return this.$store.state.count },
    user() { return this.$store.state.user },

    // 读取 getters
    isLoggedIn() { return this.$store.getters.isLoggedIn },
    userName() { return this.$store.getters.userName },

    // 带参数的 getter
    getTodoById() { return this.$store.getters.todoById },
  },
  methods: {
    // 提交 mutation
    increment() { this.$store.commit('INCREMENT') },
    setCount(n) { this.$store.commit('SET_COUNT', n) },

    // 载荷风格(对象形式)
    setCountObj() {
      this.$store.commit({ type: 'SET_COUNT', value: 10 })
    },

    // dispatch action
    async login() {
      await this.$store.dispatch('login', { username: 'admin', password: '123' })
    },
    // 对象风格 dispatch
    loginObj() {
      this.$store.dispatch({ type: 'login', username: 'admin', password: '123' })
    },
  },
}
</script>

mapState / mapGetters / mapMutations / mapActions

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

export default {
  computed: {
    // 映射 state
    ...mapState(['count', 'user', 'token']),

    // 重命名
    ...mapState({ myCount: 'count', myUser: 'user' }),

    // 映射 getters
    ...mapGetters(['isLoggedIn', 'isAdmin', 'activeTodos']),

    // 混合本地 computed
    localComputed() { return 'xxx' },
  },

  methods: {
    // 映射 mutations
    ...mapMutations(['INCREMENT', 'SET_COUNT', 'SET_THEME']),

    // 重命名
    ...mapMutations({ add: 'ADD_TODO', remove: 'REMOVE_TODO' }),

    // 映射 actions
    ...mapActions(['login', 'logout', 'fetchTodos']),

    // 混合本地 methods
    localMethod() { return 'xxx' },
  },
}
</script>

模块化

大型项目需要将 store 拆分为模块。

模块定义

js
// store/modules/user.js
const userModule = {
  namespaced: true, // 启用命名空间

  state: () => ({
    name: '',
    token: '',
    roles: [],
    permissions: [],
  }),

  getters: {
    isLoggedIn: (state) => !!state.token,
    isAdmin: (state) => state.roles.includes('admin'),
    hasPermission: (state) => (perm) => state.permissions.includes(perm),
  },

  mutations: {
    SET_USER(state, { name, roles, permissions }) {
      state.name = name
      state.roles = roles
      state.permissions = permissions
    },
    SET_TOKEN(state, token) {
      state.token = token
    },
    CLEAR_USER(state) {
      state.name = ''
      state.token = ''
      state.roles = []
      state.permissions = []
    },
  },

  actions: {
    async login({ commit }, credentials) {
      const res = await api.login(credentials)
      commit('SET_TOKEN', res.data.token)
      commit('SET_USER', res.data)
      return res.data
    },

    async logout({ commit }) {
      await api.logout()
      commit('CLEAR_USER')
    },

    async getUserInfo({ commit, state }) {
      if (!state.token) return
      const res = await api.getUserInfo()
      commit('SET_USER', res.data)
    },
  },
}

export default userModule
js
// store/modules/order.js
const orderModule = {
  namespaced: true,

  state: () => ({
    list: [],
    currentOrder: null,
    loading: false,
  }),

  getters: {
    orderCount: (state) => state.list.length,
    pendingOrders: (state) => state.list.filter((o) => o.status === 'pending'),
  },

  mutations: {
    SET_LIST(state, list) { state.list = list },
    SET_CURRENT(state, order) { state.currentOrder = order },
    SET_LOADING(state, loading) { state.loading = loading },
  },

  actions: {
    async fetchOrders({ commit, rootState }) {
      // 访问全局 state(假设全局 state 中 user 模块有 userInfo.id)
      const userId = rootState.user.userInfo.id
      commit('SET_LOADING', true)
      try {
        const res = await api.getOrders(userId)
        commit('SET_LIST', res.data)
      } finally {
        commit('SET_LOADING', false)
      }
    },
  },
}

export default orderModule

注册模块

js
// store/index.js
import user from './modules/user'
import order from './modules/order'

export default new Vuex.Store({
  // 全局 state
  state: { theme: 'light' },
  mutations: {
    SET_THEME(state, theme) { state.theme = theme },
  },

  modules: {
    user,   // 注册为 'user' 命名空间
    order,  // 注册为 'order' 命名空间
  },
})

使用命名空间模块

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

export default {
  computed: {
    // 命名空间模块的 state
    ...mapState('user', ['name', 'roles']),
    ...mapState('order', ['list', 'loading']),

    // 命名空间模块的 getters
    ...mapGetters('user', ['isLoggedIn', 'isAdmin']),
    ...mapGetters('order', ['pendingOrders']),

    // 混合全局和模块
    ...mapState(['theme']),
  },

  methods: {
    // 命名空间模块的 mutations
    ...mapMutations('user', ['SET_TOKEN']),
    ...mapMutations('order', ['SET_CURRENT']),

    // 命名空间模块的 actions
    ...mapActions('user', ['login', 'logout']),
    ...mapActions('order', ['fetchOrders']),

    // 调用其他模块的 action
    async initData() {
      await this.$store.dispatch('user/getUserInfo')
      await this.$store.dispatch('order/fetchOrders')
    },
  },
}
</script>

模块间访问

js
// 在模块的 action 中访问全局 state、getters,或调用其他模块的 action/mutation
actions: {
  async fetchOrders({ commit, rootState, rootGetters, dispatch }) {
    // rootState —— 全局 state(非当前模块的 state)
    const userId = rootState.user.id   // 访问 user 模块的 state

    // rootGetters —— 全局 getters(需带命名空间路径)
    const isLoggedIn = rootGetters['user/isLoggedIn']

    // dispatch 其他模块的 action(需带命名空间前缀,第三个参数 { root: true } 表示从根开始查找)
    await dispatch('user/getUserInfo', null, { root: true })

    // commit 全局 mutation(非当前模块的 mutation,需加 { root: true })
    commit('SET_THEME', 'dark', { root: true })
  },
}

动态注册模块

大型应用中,某些模块可能只需要在特定条件下加载。

js
// 动态注册模块
store.registerModule('dynamicModule', {
  namespaced: true,
  state: () => ({ data: null }),
  mutations: {
    SET_DATA(state, data) { state.data = data },
  },
})

// 带选项
store.registerModule(['user', 'preferences'], {
  namespaced: true,
  state: () => ({ theme: 'light', lang: 'zh-CN' }),
})

// 检查模块是否已注册
if (store.hasModule('dynamicModule')) {
  // ...
}

// 注销模块
store.unregisterModule('dynamicModule')

💡 动态注册的模块不会被 $store.state.dynamicModule 自动识别类型,需要手动声明类型。


插件系统

基本用法

js
// 自定义日志插件
const loggerPlugin = (store) => {
  // 每次 mutation 之后调用
  store.subscribe((mutation, state) => {
    console.log(`[Vuex] ${mutation.type}`, mutation.payload)
  })
}

const store = new Vuex.Store({
  plugins: [loggerPlugin],
  // ...
})

持久化插件

js
// 手动实现简单持久化
const persistencePlugin = (store) => {
  // 初始化时恢复
  const saved = localStorage.getItem('vuex-state')
  if (saved) {
    try {
      store.replaceState(JSON.parse(saved))
    } catch (e) {
      console.error('恢复 state 失败', e)
    }
  }

  // 每次 mutation 后保存
  store.subscribe((mutation, state) => {
    // 只保存需要持久化的字段
    const toSave = {
      token: state.user?.token,
      theme: state.theme,
    }
    localStorage.setItem('vuex-state', JSON.stringify(toSave))
  })
}
bash
# 推荐使用 vuex-persistedstate
yarn add vuex-persistedstate
js
import createPersistedState from 'vuex-persistedstate'

const store = new Vuex.Store({
  plugins: [
    createPersistedState({
      key: 'my-app',
      paths: ['user.token', 'theme'], // 只持久化指定字段
      storage: localStorage,           // 或 sessionStorage
    }),
  ],
})

Mutation 常量

大型项目中,用常量替代 mutation 字符串,避免拼写错误。

js
// store/mutation-types.js
export const SET_USER = 'SET_USER'
export const SET_TOKEN = 'SET_TOKEN'
export const SET_COUNT = 'SET_COUNT'
export const CLEAR_USER = 'CLEAR_USER'
js
// store/modules/user.js
import { SET_USER, SET_TOKEN, CLEAR_USER } from '../mutation-types'

export default {
  namespaced: true,
  mutations: {
    [SET_USER](state, user) { state.user = user },
    [SET_TOKEN](state, token) { state.token = token },
    [CLEAR_USER](state) { state.user = null; state.token = '' },
  },
}

严格模式

什么是严格模式?

严格模式是 Vuex 的一种调试功能。开启后,如果你不通过 mutation 而是直接修改 state,Vuex 会抛出错误,提醒你这样做是不对的。

为什么要用严格模式?

Vuex 的核心理念是"单向数据流":state → view → action → mutation → state。如果绕过 mutation 直接修改 state,Vue Devtools 就无法追踪数据变化,调试会变得很困难。

严格模式帮你强制执行这个规则,在开发阶段及时发现"偷懒"的代码。

什么时候用?

  • 开发环境:开启,帮助发现非法的状态修改
  • 生产环境:必须关闭,因为严格模式会在每次 mutation 时深拷贝 state,性能损耗很大
js
const store = new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  // ...
})
  • 开启后,直接修改 state 会抛错(只能通过 mutation 修改)
  • 内部使用 deepCopy 监听 state 变化,性能损耗大
  • 生产环境必须关闭

Vuex vs EventBus

特性VuexEventBus
数据流单向,可追踪无序,难以追踪
调试Vue Devtools 完整支持不支持
持久化插件支持不支持
适用规模中大型项目小型项目
复杂度较高极低

常见坑点

1. Mutation 必须同步

js
// ❌ 异步操作放在 mutation
mutations: {
  async FETCH_DATA(state) {
    const res = await api.getData() // 不要这样做!
    state.data = res.data
  },
}

// ✅ 异步操作放在 action
actions: {
  async fetchData({ commit }) {
    const res = await api.getData()
    commit('SET_DATA', res.data)
  },
}

2. 忘记开启命名空间

js
// ❌ 模块默认共享全局命名空间
const userModule = {
  state: () => ({ name: '' }),
  mutations: {
    SET_NAME(state, name) { state.name = name },
  },
}

// 如果另一个模块也有 SET_NAME,会冲突

// ✅ 开启命名空间
const userModule = {
  namespaced: true,
  // ...
}

3. 直接修改数组项

js
// ❌ 严格模式下报错
this.$store.state.list[0].done = true

// ✅ 通过 mutation 修改
this.$store.commit('TOGGLE_TODO', id)

4. 大对象深拷贝性能

js
// 严格模式下每次 mutation 都深拷贝 state
// state 很大时会有明显性能问题
// 解决:生产环境关闭严格模式

参考

💡 Vuex 3 和 4 的核心 API 基本一致,主要差异在于安装方式:Vuex 4 使用 createStore() 替代 new Vuex.Store(),并通过 app.use(store) 注册到 Vue 应用实例。

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准