# Vuex 是什么? Vuex 是一个专为 Vue.js 应用程序开发的**状态管理模式**。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 [devtools extension](https://github.com/vuejs/vue-devtools),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。 ### 什么是“状态管理模式”? 让我们从一个简单的 Vue 计数应用开始: ``` js new Vue({ // state data () { return { count: 0 } }, // view template: `
{{ count }}
`, // actions methods: { increment () { this.count++ } } }) ``` 这个状态自管理应用包含以下几个部分: - **state**,驱动应用的数据源; - **view**,以声明方式将 **state** 映射到视图; - **actions**,响应在 **view** 上的用户输入导致的状态变化。 以下是一个表示“单向数据流”理念的极简示意:

但是,当我们的应用遇到**多个组件共享状态**时,单向数据流的简洁性很容易被破坏: - 多个视图依赖于同一状态。 - 来自不同视图的行为需要变更同一状态。 对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。 因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为! 另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护。 这就是 Vuex 背后的基本思想,借鉴了 [Flux](https://facebook.github.io/flux/docs/overview.html)、[Redux](http://redux.js.org/)、和 [The Elm Architecture](https://guide.elm-lang.org/architecture/)。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。 ![vuex](./images/vuex.png) ### 什么情况下我应该使用 Vuex? 虽然 Vuex 可以帮助我们管理共享状态,但也附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 [global event bus](https://cn.vuejs.org/v2/guide/components.html#非父子组件通信) 就足够您所需了。但是,如果您需要构建是一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是: > Flux 架构就像眼镜:您自会知道什么时候需要它。 # 核心概念 在这一章,我们将会学到 Vuex 的这些核心概念。他们是: - [State](state.md) - [Getter](getters.md) - [Mutation](mutations.md) - [Action](actions.md) - [Module](modules.md) 深入理解所有的概念对于使用 Vuex 来说是必要的。 让我们开始吧。 # State ### 单一状态树 Vuex 使用**单一状态树**——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 ([SSOT](https://en.wikipedia.org/wiki/Single_source_of_truth))”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 单状态树和模块化并不冲突——在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中。 ### 在 Vue 组件中获得 Vuex 状态 那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在[计算属性](https://cn.vuejs.org/guide/computed.html)中返回某个状态: ``` js // 创建一个 Counter 组件 const Counter = { template: `
{{ count }}
`, computed: { count () { return store.state.count } } } ``` 每当 `store.state.count` 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。 然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。 Vuex 通过 `store` 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 `Vue.use(Vuex)`): ``` js const app = new Vue({ el: '#app', // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: `
` }) ``` 通过在根实例中注册 `store` 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 `this.$store` 访问到。让我们更新下 `Counter` 的实现: ``` js const Counter = { template: `
{{ count }}
`, computed: { count () { return this.$store.state.count } } } ``` ### `mapState` 辅助函数 当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 `mapState` 辅助函数帮助我们生成计算属性,让你少按几次键: ``` js // 在单独构建的版本中辅助函数为 Vuex.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 } }) } ``` 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 `mapState` 传一个字符串数组。 ``` js computed: mapState([ // 映射 this.count 为 store.state.count 'count' ]) ``` ### 对象展开运算符 `mapState` 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 `computed` 属性。但是自从有了[对象展开运算符](https://github.com/sebmarkbage/ecmascript-rest-spread)(现处于 ECMASCript 提案 stage-3 阶段),我们可以极大地简化写法: ``` js computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) } ``` ### 组件仍然保有局部状态 使用 Vuex 并不意味着你需要将**所有的**状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。 # Getter 有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数: ``` js computed: { doneTodosCount () { return this.$store.state.todos.filter(todo => todo.done).length } } ``` 如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。 Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 Getter 接受 state 作为其第一个参数: ``` js 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) } } }) ``` Getter 会暴露为 `store.getters` 对象: ``` js store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] ``` Getter 也可以接受其他 getter 作为第二个参数: ``` js getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } } ``` ``` js store.getters.doneTodosCount // -> 1 ``` 我们可以很容易地在任何组件中使用它: ``` js computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } } ``` 你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。 ```js getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } } ``` ``` js store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false } ``` ### `mapGetters` 辅助函数 `mapGetters` 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性: ``` js import { mapGetters } from 'vuex' export default { // ... computed: { // 使用对象展开运算符将 getter 混入 computed 对象中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } } ``` 如果你想将一个 getter 属性另取一个名字,使用对象形式: ``` js mapGetters({ // 映射 `this.doneCount` 为 `store.getters.doneTodosCount` doneCount: 'doneTodosCount' }) ```