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 storejs
// 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 userModulejs
// 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-persistedstatejs
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
| 特性 | Vuex | EventBus |
|---|---|---|
| 数据流 | 单向,可追踪 | 无序,难以追踪 |
| 调试 | 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 应用实例。