Vuex 状态管理:Vue 2 的集中式 Store
什么是 Vuex?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
简单来说,当你的应用遇到多个组件共享状态时,单纯依靠组件之间的传参会很快变得混乱。Vuex 把组件的共享状态抽取出来,以一个全局单例 Store 进行管理,组件树中的任何组件都能直接访问状态或触发变更,结构清晰、可维护性极高。
核心概念:Store、State、Getters、Mutations、Actions
Store —— 一切的中心
每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
State —— 单一状态树
Vuex 使用单一状态树——用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这意味着每个应用将仅仅包含一个 store 实例。
在组件中获取 Vuex 状态的最简单方法是在计算属性中返回某个状态:
// 在组件内部
computed: {
count() {
return this.$store.state.count
}
}
通常我们会使用 mapState 辅助函数,当一个组件需要获取多个状态时,它能帮助生成计算属性:
import { mapState } from 'vuex'
export default {
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState(state) {
return state.count + this.localCount
}
})
}
Getters —— 从 State 派生出新状态
有时我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数。如果有多个组件需要用到此属性,我们复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
定义 getter:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '吃饭', done: true },
{ id: 2, text: '睡觉', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
})
在组件中使用 mapGetters 辅助函数:
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['doneTodos', 'doneTodosCount'])
}
}
Mutations —— 更改状态的唯一方式
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment(state) {
state.count++
}
}
})
你不能直接调用一个 mutation handler。要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:
this.$store.commit('increment')
你还可以提交载荷(Payload),也就是额外的参数:
mutations: {
incrementBy(state, n) {
state.count += n
}
}
// 调用
store.commit('incrementBy', 10)
重要规则:Mutation 必须是同步函数。因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
Actions —— 处理异步操作
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
注册一个简单的 action:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
},
// 实践中经常使用参数解构来简化代码
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
Action 通过 store.dispatch 方法触发:
this.$store.dispatch('incrementAsync')
同样支持载荷方式和对象风格的提交。Actions 支持与 async/await 组合来完成更复杂的异步流程。组件中使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用。
安装与基础配置
在你的 Vue 2 项目中安装 Vuex:
npm install vuex@3 --save
创建 store 文件,例如 src/store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 你的状态
},
getters: {},
mutations: {},
actions: {}
})
在 Vue 实例中注入 store:
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store, // 注入所有组件
render: h => h(App)
}).$mount('#app')
完成这两步后,每个组件内部都可以通过 this.$store 访问到 store 实例。
模块化:Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++ } },
actions: { incrementIfOdd({state, commit}) { ... } },
getters: { doubleCount(state) { return state.count * 2 } }
}
const moduleB = {
state: () => ({ name: 'moduleB' }),
// ...
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState。
默认情况下,getter、mutation 和 action 是注册在全局命名空间下的。如果你想模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
实战示例:一个简单的计数器
让我们通过一个完整的计数器示例来串联所有知识点。
store 定义 (src/store/index.js):
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state, payload = 1) {
state.count += payload
},
decrement(state) {
state.count--
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment', 5)
}, 1000)
}
}
})
组件中使用 (Counter.vue):
<template>
<div>
<p>{{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementAsync">Async + (after 1s)</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['increment', 'decrement']),
...mapActions(['incrementAsync'])
}
}
</script>
严格模式与表单处理
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
开启严格模式:
const store = new Vuex.Store({
// ...
strict: true
})
不要在发布环境下启用严格模式! 严格模式会深度监测状态树来检测不合规的状态变更,会影响性能。可以利用环境变量:
strict: process.env.NODE_ENV !== 'production'
处理 v-model 和 Vuex 状态时,由于 v-model 会试图直接修改状态,在严格模式下会抛出错误。解决办法是使用带有 get 和 set 的计算属性,或者用 :value + @input 事件替代。
项目结构组织
对于稍大一些的应用,推荐将 Vuex 相关代码按职责分割到不同文件中:
src
└── store
├── index.js # 组装模块并导出 store
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 商品模块
每个模块使用 namespaced: true,保持高内聚低耦合。
最佳实践与常见误区
- 不要在 mutation 中执行异步操作:始终让 mutation 保持同步,异步逻辑放到 action 里。
- 表单处理不要直接修改 state:使用计算属性的 get/set 或利用
@input和:value。 - 合理使用 map 辅助函数:对于对象展开运算符
...mapState的使用,注意不要覆盖已有的计算属性或方法。 - 模块划分不要过早:在应用规模尚小时,不要急于划分模块,保持简单。随着业务增长自然地进行拆分。
- 调试:始终开启 Vue devtools,它能够提供 state 快照、时间旅行等强大功能,帮助你快速定位问题。
Vuex 的学习曲线平缓,核心思想集中。掌握这五个核心概念后,即可轻松管理绝大多数 Vue 2 应用的状态,让代码更具可预测性与可维护性。