Vue系列 | Vuex状态管理器

What 什么是Vuex

官方的定义是这样的:

1
Vuex is a state management pattern + library for Vue.js applications.

显然它是Vue官方为Vue.js提供的库,功能是数据状态管理。它是在一个应用的层级对所有组件做集中状态管理的工具。来看一下Vuex在应用中的结构:
Alt text

定义中提到了state management pattern,即状态管理模式,那问题就来了为什么需要这种设计模式呢,普通的模式什么样呢,多加一层有什么好处呢,软件开发中有一条不成文的规律:多加一层。难度就下降一倍。我们来看看这样的设计是否达到了这样的效果。

我们来看一下正常情况下的模式:
Alt text
即所谓的State–View–Action模式:

这是一个简化的自包含系统,遵循单向数据流的核心原则。

View:用于接收用户输入

Actions:响应用户输入来改变状态

State:接收用户数据,并把数据传递到View层来显示

然而当多个View共享一个State时,这种平衡被破坏了。

  • 多个view可能依赖相同的state
  • Actions来自不同的View需要改变相同的state
    对于问题一, 如果是层级嵌套的组件View的状态更新我们可以通过props来传递,但是层级过多的时候很麻烦而同级组件不起作用。
    对于问题二,我们经常需要查询父子空间的应用重新存储State,或通过事件来更改和同步多个使用状态的地方,会导致代码脆弱,不可维护。

基于以上原因,我们抽象出了图一所谓的状态管理模式,即:把共享的状态取出来,变成一个全局单例。无论是访问状态还是追踪改变,都通过这个单例进行。

这就是Vuex背后的核心思想,通过定义和分离状态管理的概念,使views和states保持了独立,从而给了我们更多的代码结构和维护性。

Vuex是专门为Vue.js定制开发的,利用的Vue.js的响应式系统和高效更新的优势,是Vue项目下状态管理插件的首选。

Why 为什么要使用Vuex

其实有时候知道了what,我们也就知道了why。自己定义一个全局的单例貌似也可以实现同样的效果,我们来具体说说为什么Vuex和全局单例是不同的:

  1. Vuex作为应用的存储中心,它的数据是响应式的。如果状态发生改变,界面会进行高效自动更新。
  2. 你并不能够直接改变存储的状态,我们其实是通过committing mutations的声明来改变状态的,这使得我们的改变是可以被如devtools工具追踪到,有助于bug调试。

When 何时使用

Vuex虽然帮我们共享了状态管理,但也额外引入了更多的概念和样板文件。如果你的应用比较简单,你就用store pattern,一种追踪变化和实时响应的存储模式。如果你是中大型项目,尤其是跨层级的组件间共享状态你自然可以尝试Vuex。

How 如何使用

  1. 安装npm install vuex --save
  2. 引用

    1
    2
    import Vuex from 'vuex'
    Vue.use(Vuex)
  3. 创建一个存储实例

    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
    26
    27
    28
    29
    30
    31
    32
    const store = new Vuex.Store({
    state: {
    count: 0,
    todos: [
    { id: 1, text: '...', done: true },
    { id: 2, text: '...', done: false}
    ]
    },
    mutations: {
    increment (state, n) {
    state.count += n
    }
    },
    actions: {
    increment({commit}) {
    setTimeout(() => {
    commit('increment', 2)
    },3000)
    }
    },
    getters: {
    doneTodos: state => {
    return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
    },
    getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
    }
    }
    })

大家看到上面声明了一些对象关键字如statemutationsgetters,先别急,后面我们一个一个讲清楚。

  1. 挂载到Vue实例下面

    1
    2
    3
    4
    5
    // main.js 入口文件
    new Vue({
    store,
    render: h => h(App),
    }).$mount('#app')
  2. 在其他组件中使用

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// App.vue
<template>
<div id="app">
<div>
{{ count }}
<button @click="$store.commit('increment',2)">count++</button>
<button @click="$store.dispatch('increment')">count++</button>
</div>
<div>
<div>
<span>
doneTodos:
</span>
{{ $store.getters.doneTodos }}
</div>
<div>
<span>
doneTodosCount:
</span>
{{ doneTodosCount }}
</div>
<div>
<span>
getTodoById(2):
</span>
{{ $store.getters.getTodoById(2) }}
</div>
</div>
</div>
</template>

<script>
export default {
name: 'app',
computed: {
count () {
return this.$store.state.count
},
doneTodosCount () {
return this.$store.getters.doneTodosCount
},

}
}
</script>

核心概念

State

State是一个全局唯一的状态树

如何在Vue组件中获取Vuex的状态

方法一:由于state的存储是响应式的,所以我们可以在合适的计算属性里使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const store = new Vuex.Store({
state: {
count: 1
}
})
const Counter = {
template: `<div> {{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
var vm = new Vue({
el:'#example',
components: {Counter},
template:`<counter></counter>`
})

进一步优化

这样每当在其他组件使用的时候,我们都需要导入store,Vue为我们提供了一个store操作符,可以全局注入所有的子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Counter = {
template: `<div> {{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
var vm = new Vue({
el:'#example',
store,
components: {Counter},
template:`<counter></counter>`
})

在创建Vue根实例的时候声明store,而后使用this.$store的方式访问。

注意:
我这里是通过<script>标签对导入的Vuex,如果你是通过包管理工具下载而后通过import命令导入的包,还需要声明Vue.use(Vuex)

mapState 助手

在组件中访问多个属性或getters的时候,它可以帮我们简化写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
import { mapState } from 'vuex'
export default {
name: 'HelloWorld',
data () {
return {
localCount: 5
}
},
computed: mapState({
// 可以使用箭头函数访问
count: state => state.count,
// 给count起一个别名
countAlias: 'count',
// 当使用本地数据的时候必须使用函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
</script>

在被映射的计算属性和state的子树名称相同时,可以传递一个字符串数组:

1
2
3
computed: mapState([
'count'
])

对象拓展运算符

1
2
3
4
5
6
computed: {
localComputed () { return 1 },
...mapState({
count: state => state.count
})
}

可以将mapState返回的对象结合其他的本地属性使用。

Getters

有时候我们需要基于state做一些计算,例如从一个代办事项里过滤已经做完的事情:

1
2
3
4
5
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}

如果多个地方用到这个功能,我们需要写多个函数或取出封装为一个公共组件在其他函数里导入,都不是好主意。

getters相当于Vue组件的计算属性,可以缓存它的依赖数据,当依赖改变的时候才会重新计算:

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

怎么访问Getters

  1. 属性方式访问

getters通过store.getters返回对象,所以我们可以直接在组件中使用:

1
2
3
4
5
computed: {
doneTodos() {
return this.$store.getters.doneTodos // [ { "id": 1, "text": "...", "done": true } ]
}
}

getters可以接收其他的getters作为第二个参数:

1
2
3
4
5
6
7
8
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length // 1
}
}

  1. 方法方式访问

我们可以返回一个函数,这在查询数组的时候尤其有用

1
2
3
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}

1
this.$store.getters.getTodoById(2) //  { "id": 2, "text": "...", "done": false }

注意:
通过方法调用没办法缓存,每一次都会重新计算

mapGetters助手

它负责把store getters映射到本地属性,所以使用时代码我们可以简化如下:

1
2
3
4
5
6
computed: {
...mapGetters([
'doneTodos',
'doneTodosCount'
])
}

可以用以下方式起一个别名:

1
2
3
...mapGetters({
doneCount: 'doneTodosCount'
})

Mutations

mutations其实是Vuex里面改变state的唯一方式,也就是说你必须通过提交一个mutation来改变state的状态,不能直接操作state。它和事件比较像,有一个字符串的类型和一个处理器,这个处理器函数来执行实际state的改变。它接收state作为第一个参数。

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count ++
}
}
})

需要注意的是你不能直接调用它,它更像是一个事情注册,当类型为increment的mutation函数被触发时,调用这个处理器。为了调用这个处理器,我们需要使用store.commit('increment')

1
2
3
4
5
<template>
<div>
<button @click="$store.commit('increment')">++</button> // 1
</div>
</template>

使用Payload

你可以传递给store.commit一个额外的参数,payload将会被调用。

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

1
<button @click="$store.commit('increment',2)">++</button>

一般payload可能包含多个字段,这样的话我们可以更具体的访问:

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

1
<button @click="$store.commit('increment',{amount: 2})">++</button>

对象方式提交

我们可以采用对象方式提交:

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

此时,整个对象会被传递给payload,所以,mutation的处理方式是相同的:

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

对于mutation的类型使用常量

对于mutation的类型我们可以使用常量,这样的话可以使用语法高亮,不容易拼写错误,而且把他们单独放在一个文件中,对于团队开发会比较方便,直接能在一个文件里面看到所有的mutation,这是一个常见的使用惯例。

1
2
// mution-type.js
export const INCREMENT = 'increment'

1
2
3
4
5
6
import { INCREMENT } from './mutaion-types'
mutations: {
[INCREMENT] (state, payload) {
state.count += payload.amount
}
}

Mutations必须是同步的

由于devtools里面调试时需要抓取state改变前后的对比数据,但是异步函数不知道什么时候调用完成,所以没有办法追踪。

在组件中使用Mutations

我们可以使用this.$store.commit(‘xxxx’)来正常提交,也可以使用mapMutations助手(必须把store注入根节点)做映射:

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

1
2
3
4
5
6
7
8
// HTML
<button @click="increment({amount: 3})">++</button>
// JS
methods: {
...mapMutations([
'increment'
])
}

还可以设置别名:

1
2
3
4
5
6
7
8
// HTML
<button @click="add({amount: 3})">++</button>
// JS
methods: {
...mapMutations({
add: 'increment'
})
}

Actions

为异步操作而生

  • 提交操作给mutations而不是直接改变state
  • 可以执行任何异步操作

先看一段声明:

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

context包括了所有store上下文的所有方法和属性,所以他可以访问context.commit,context.getters,context.state

在实践中我们使用ES2015的参数解构来简化我们的代码,尤其是需要调用commit多次时:

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

发送Actions

我们使用store.dispatch方法来触发Actions对象。

1
store.dispatch('increment')

之所以搞的这么麻烦是因为Actions支持异步操作:

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

它在使用的时候和Mutations支持相同的payload和对象格式:

1
2
3
4
5
// payload格式
<button @click="$store.dispatch('increment', {amount: 3})">++</button>

// 对象格式
<button @click="$store.dispatch({type: 'increment', amount: 3})">++</button>

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

它在组件中的用法和Mutations相同,不在赘述。

1
2
3
4
5
6
7
// JS
import { mapActions } from 'vuex'
methods: {
...mapActions({
add: 'increment'
})
}

组合Actions

Actions既然是异步的,那么我们怎么知道它什么时候执行完呢,Actions的处理器可以接收返回一个Promise:

1
2
3
4
5
6
7
8
actionA({commit}, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment', payload)
resolve('finish')
}, 1000)
})
}

所以现在可以这样接收:

1
2
3
this.$store.dispatch({type: 'actionA', amount: 3}).then((res)=> {
console.log(res) // finish
})

我们还可以在另一个action里嵌套:

1
2
3
4
5
actionB({dispatch,commit},payload) {
return dispatch('actionA',payload).then(() => {
commit('incrementOne')
})
}

1
this.$store.dispatch('actionB',{amount: 4})

当然,我们可以使用async/wait组合的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
incrementValue({state, commit}, payload) {
return new Promise((resolve, reject) => {
// setTimeout(() => {
commit('increment', payload)
resolve('finish')
}, 1000)
},
async actionA({dispatch, commit}, payload) {
await dispatch('incrementValue', payload)
},
async actionB({dispatch,commit},payload) {
await dispatch('actionA',payload)
setTimeout(() => {
commit('incrementOne')
},1000)

}

1
this.$store.dispatch('actionB',{amount: 4})

有时候我们action处理器来自不同的模块,我们需要等到所有的action处理完毕后返回一个Promise。

模块

为了防止store在一个文件中越来越大,Vuex支持模块划分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import ModuleA from './moduleA'
import ModuleB from './moduleB'
const store = new Vuex.Store({
state: {
count: 0,
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false}
]
},
modules: {
a:ModuleA,
b:ModuleB
}
})

1
2
3
4
5
6
7
8
9
10
11
12
// ModuleA.js
export default{
state: {
count:10
}
}
// ModuleB.js
export default {
state: {
count:20
}
}
1
2
3
4
5

<div>
{{ $store.state.a }} // { "count": 10 }
{{ $store.state.b }} // { "count": 20 }
</div>

在模块内部,mutations和getters里的参数是接收文件内自己的状态,actions的context.state也是一样的,如果我们要访问根节点上的状态,我们需要通过context.rootState导出:

1
2
3
4
5
6
7
8
actions: {
increment({ state , commit, rootState}) {
console.log(rootState.count)
if (rootState.count === 0) {
commit('increment')
}
}
}

默认情况下,模块内外的事件都是被注册到全局命名空间的,所以访问actions/mutaions的时候,模块间的相同事件是同时响应的,如果想要自包含,需要添加namespaced: true,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ModulesA.js
export default {
namespaced: true,
state: {
count:10
},
mutations: {
increment (state) {
state.count++
}
},

actions: {
someAction ({state, commit,rootState,rootGetters}) {
dispatch('someOtherAction') // moduleA/someOtherAction
dispatch('someOtherAction', null, {root: true}) // someOtherAction
},
someOtherActon(ctx, payload) {
// ...
}
}

访问的时候加上路径就可以:

1
<button @click="$store.dispatch('a/increment')">moduleA++</button>

模块间也是可以继承和嵌套的,具体看:vuex-modlues

在命名空间下访问全局资源

如果想使用全局的state和getters,那么rootStaterootGetters作为getter函数的第三个和四个参数,他们也被封装到context上下文传递给 aciton 函数。
如果想要dispatchcommit到全局命名空间,需要添加{root: true}为第三个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getters: {
someGetter(state, getters, rootState, rootGetters){
getters.someOtherGetter // mudlueA/someOtherGetter
rootGetters.someOtherGetter // someOtherGetter
},
someOtherGetter: state => { //... }
},
actions: {
someAction ({state, commit,rootState,rootGetters}) {
dispatch('someOtherAction') // moduleA/someOtherAction
dispatch('someOtherAction', null, {root: true}) // someOtherAction
},
someOtherActon(ctx, payload) {
// ...
},
}

在命名空间下注册全局资源

在actions对象内,声明action名称对象,在该对象内使用root:true,并把原 action 内的事件处理给到函数handler

1
2
3
4
5
6
7
8
9
10
// ModuleA.js
actions: {
add:{
root: true,
handler ({state, commit}, payload) {
commit('increment') // moduleA/increment
commit ('increment', payload, {root: true}) // increment
}
}
}

命名空间如何绑定助手

通常情况下,这样来写(以state为例):

1
2
3
4
5
6
import { mapState } from 'vuex'
computed: {
...mapState({
namespacedCount: state => state.a.count
}),
}

如果mapState里面的对象比较多,写起来有点啰嗦,所以我们可以把命名空间模块写为mapState的第一个参数:

1
2
3
4
5
computed: {
...mapState('a',{
namespacedCount: state => state.count
}),
}

如果mapState,mapGetters,mapMutions都用到这个命名空间,我们没有必要在每一个里面都写一个空间命名参数,可以使用createNamespacedHelpers做进一步简化:

1
2
3
4
5
6
7
import { createNamespacedHelpers } from 'vuex';
const { mapState } = createNamespacedHelpers('a')
computed: {
...mapState({
namespacedCount: state => state.count
}),
}

总结

What | 首先我们介绍了什么是Vuex,它是Vue官方提供的状态管理工具,利用了Vue的响应式特性。

Why | 进而我们指出了为什么要使用Vuex,基于它的响应式更新机制及辅助调试。

When | 我们也介绍了你应该什么时候使用Vuex,如何权衡。

How | 我们花了90%以上的篇幅介绍的Vuex的使用细节:

  • 如何安装
  • 如何声明
  • 如何访问

接着我们介绍了Vuex的六大核心概念,他们分别是:

  1. State
    全局状态树,类比组件中的data属性。
  2. Getters
    类比组件中的computed属性,可以缓存数据,用于计算逻辑。
  3. Mutations
    操作state的唯一方式,类比组件中的method
  4. Actions
    做异步事件处理,只能提交给mutations,不能直接操作state,同样类比组件中的method
  5. Mudules
    用于复杂场景下的模块划分,可以理解为封装。

参考:
Vuex