Vuex 状态管理

Vuex 简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态。

何时使用 Vuex?

  • 当开发中大型单页应用时,需要考虑如何更好地在组件外部管理状态,此时,Vuex 会是一个好选择。

  • 但当应用太过简单,最好不要使用 Vuex,使用 Vuex 会显得更加繁琐冗余,这时只需要用到简单的 store 模式即可满足需求。

什么是 “状态管理模式”?

来自于官网的例子:Vue 计数应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

“单向数据流” 理念

“单向数据流” 理念
“单向数据流” 理念

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的 “视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

Vuex 的组成结构

Vuex 的组成结构
Vuex 的组成结构

Vuex 目录结构

目录
目录

Vuex 的核心概念

Store

vuex 中最关键的是 store 对象,这是 vuex 的核心。可以说,vuex 这个插件其实就是一个 store 对象,每个 vue 应用仅且仅有一个 store 对象。

store 的要点

    1. store 中存储的状态是响应式的,当组件从 store 中读取状态时,如果 store 中的状态发生了改变,那么相应的组件也会得到更新;
    1. 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径是提交 (commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化。

如何创建 Store

创建 Store,代码示例:

1
const store = new Vuex.Store({...});

可见,store 是 Vuex.Store 这个构造函数 new 出来的实例。在构造函数中可以传一个对象参数。这个参数中可以包含 5 个对象:

  • state – 存放状态
  • getters – state 的计算属性
  • mutations – 更改状态的逻辑,同步操作
  • actions – 提交 mutation,异步操作
  • mudules – 将 store 模块化

完整的 Store 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 存放状态
},
getters: {
// state的计算属性
},
mutations: {
// 更改state中状态的逻辑,同步操作
},
actions: {
// 提交mutation,异步操作
},
// 如果将store分成一个个的模块的话,则需要用到modules。
//然后在每一个module中写state, getters, mutations, actions等。
modules: {
a: moduleA,
b: moduleB,
// ...
}
})

State

单一状态树

Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个 “唯一数据源 (SSOT)” 而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突 —— 在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中。

存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则。

如何在在 Vue 组件中获得 Vuex 状态?

由于 Vuex 的状态存储是响应式的,从 store 实例周读取状态最简单的方法就是在计算属性中返回某个状态

方法一:

1
2
3
4
5
6
7
8
9
10
11
// 创建一个 Counter 组件
import store from 'store';
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
// 获取store中的状态
return store.state.count
}
}
}

每当 store.state.count 变化的时候,都会重新求取计算属性,并且触发更新相关联的 DOM。

缺点:然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。

方法二:
Vuex 通过 store 选项,提供了一种机制将状态从根组件 “注入” 到每一个子组件中(需调用 Vue.use (Vuex)):

1
2
3
4
5
6
7
8
9
10
11
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。让我们更新下 Counter 的实现:

1
2
3
4
5
6
7
8
9
10
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
// store 实例被注入到根组件下所有子组件中,
// 通过 this.$store.state.stateName 获取 store 中的状态
return this.$store.state.count
}
}
}

mapState 辅助函数

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
// 此处的state即为store里面的state
count: state => state.count,

// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',

// 为了能够使用 `this` 获取局部状态,保证this指向组件对象,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}

上面是通过 mapState 的对象来赋值的,当映射的计算属性的名称与 state 的子节点名称相同时,还可以通过 mapState 的数组来赋值:

1
computed: mapState(['count']);

这种方式很简洁,但是组件中的 state 的名称就跟 store 中映射过来的同名

对象扩展运算符

mapState 函数返回的是一个对象。我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符 (opens new window),我们可以极大地简化写法:

1
2
3
4
5
6
7
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}

Getters

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数。此时可以用到 getters,getters 可以看作是 store 的计算属性,其参数为 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
 state: {
   todos: [
     {id: 1, text: 'reading', done: true},
     {id: 2, text: 'playBastketball', done: false}
   ]
 },
 getters: {
   doneTodos: state => {
     return state.todos.filter(todo => todo.done);
   }
 }
});

获取 getters 里面的状态

方法一:通过属性访问
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

1
store.getters.doneTodos //  [{ id: 1, text: 'reading', done: true }]

Getter 也可以接受其他 getter 作为第二个参数:

1
2
3
4
5
6
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
1
store.getters.doneTodosCount // -> 1

在任何组件中使用它:

1
2
3
4
5
6
//在组件中,则要写在计算属性中,
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

方法二:通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

1
2
3
4
5
6
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
1
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

方法三:使用 mapGetters 辅助函数获取 getters 里面的状态

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

1
2
3
4
import {mapState, mapGetters} from 'vuex';
computed: {
...mapState(['increment']),
...mapGetters(['doneTodos'])

如果你想将一个 getter 属性另取一个名字,使用对象形式:

1
2
3
4
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,store.commit (‘increment’)。

Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。

提交载荷(Payload)

方法一:
你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):

1
2
3
4
5
6
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
1
store.commit('increment', 10)

方法二:
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

1
2
3
4
5
6
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
1
2
3
store.commit('increment', {
amount: 10
})

方法三:
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

1
2
3
4
store.commit({
type: 'increment',
amount: 10
})

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

1
2
3
4
5
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}

注意:mutation 必须是同步函数,不能是异步的,这是为了调试的方便。

在组件中提交 mutations

你可以在组件中使用 this.$store.commit (‘xxx’) 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
方法 一:在组件的 methods 中提交
在组件中使用 this.$store.commit (‘xxx’) 提交 mutation

1
2
3
4
5
methods: {
increment(){
this.$store.commit('increment');
}
}

方法 二:使用 mapMutaions
用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { mapMutations } from 'vuex'

export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}

// 因为mutation相当于一个method,所以在组件中,可以这样来使用
<button @click="increment">+</button>

Actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

因为 mutations 中只能是同步操作,但是在实际的项目中,会有异步操作,那么 actions 就是为了异步操作而设置的。这样,就变成了在 action 中去提交 mutation,然后在组件的 methods 中去提交 action。只是提交 actions 的时候使用的是 dispatch 函数,而 mutations 则是用 commit 函数。

注册一个简单的 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

实践中,我们会经常用到 ES2015 的 参数解构 (opens new window) 来简化代码:(特别是我们需要调用 commit 很多次的时候):

1
2
3
4
5
actions: {
increment ({ commit }) {
commit('increment')
}
}

分发 Action

Action 通过 store.dispatch 方法触发:

1
store.dispatch('increment')

我们可以在 action 内部执行异步操作:

1
2
3
4
5
6
7
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}

Actions 支持同样的载荷方式和对象方式进行分发:

1
2
3
4
5
6
7
8
9
10
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})

// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})

来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}

注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)

在组件中分发 action

你在组件中使用 this.$store.dispatch (‘xxx’) 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapActions } from 'vuex'

export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}

组合 Actions

因为 action 是异步的,那么我们需要知道这个异步函数什么时候结束,以及等到其执行后,会利用某个 action 的结果。这个可以使用 promise 来实现。在一个 action 中返回一个 promise,然后使用 then () 回调函数来处理这个 action 返回的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
actions:{
 actionA({commit}){
   return new Promise((resolve, reject) => {
     setTimeout(() => {
       commit('someMutation');
       resolve();
     },1000);
   })
 }
}

// 这样就可以操作actionA返回的结果了
store.dispatch('actionA').then(() => {
 // dosomething ...
});

// 也可以在另一个action中使用actionA的结果
actions: {
 // ...
 actionB({ dispatch, commit }){
   return dispatch('actionA').then(() => {
     commit('someOtherMutation');
   })
 }
}

Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

module 是为了将 store 拆分后的一个个小模块,这么做的目的是因为当 store 很大的时候,分成模块的话,方便管理。

每个 module 拥有自己的 state, getters, mutation, action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const moduleA = {
   state: {...},
   getters: {...},
   mutations: {....},
 actions: {...}
}

const moduleB = {
   state: {...},
   getters: {...},
   mutations: {....},
 actions: {...}
}

const store = new Vuex.Store({
 modules: {
   a: moduleA,
   b: moduleB
 }
});

store.state.a // 获取moduleA的状态
store.state.b // 获取moduleB的状态

模块的局部部状态

对于模块内部的 mutation 和 getter,接受的第一个参数是模块的局部状态 state。

根结点的状态为 rootState。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},

getters: {
doubleCount (state) {
return state.count * 2
}
}
}

模块的动态注册

在模块创建之后,可以使用 store.registerModule 方法来注册模块。

1
2
3
store.registerModule('myModule', {
// ...
});

依然的,可以通过 store.state.myModule 来获取模块的状态。

可以使用 store.unregisterModule (moduleName) 来动态的卸载模块,但是这种方法对于静态模块是无效的(即在创建 store 时声明的模块)。

含有 vuex 的项目的结构

应该遵循的规则

    1. 应用层级的状态都应该集中在 store 中
    1. 提交 mutation 是更改状态 state 的唯一方式,并且这个过程是同步的。
    1. 异步的操作应该都放在 action 里面