Vuex详解

什么是Vuex?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

在Vue中,多组件的开发给我们带来了很多的方便,但同时当项目规模变大的时候,多个组件间的数据通信和状态管理就显得难以维护。而Vuex就此应运而生。将状态管理单独拎出来,应用统一的方式进行处理,在后期维护的过程中数据的修改和维护就变得简单而清晰了。这个状态管理模式包含以下三个部分:

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

简单说,就是用户界面触发动作(Action)进而改变对应状态(State),从而反映到视图(View)上,所以Vuex采用的是单向数据流的方式来管理数据的。官网给出以下图,很好做出示意:

使用Vuex

下面我是通过直接引用vue.js和Vuex.js本地文件来介绍Vuex的使用,如果是使用vue脚手架的,你可以这样来使用:

安装:

1
npm install --save vuex

引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vuex from 'vuex'
import Vue from 'vue'

Vue.use(Vuex) //作为插件使用

//定义容器
const store = new Vuex.Store({

state:{...},
mutations:{...},
actions:{...}
...
})

export default store

注入根实例:

1
2
3
4
5
6
import store from  xxx

new Vue({
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store
})

不管是否使用脚手架,Vuex的使用方法是一样的。接下来,就用具体的例子来介绍Vuex的使用。

不过在这之前,我们需要知道五个Vue的核心概念,它们分别是:

  • State
  • Mutation
  • Action
  • Getter
  • Module

我们学习Vuex的使用,就是基于这五个概念的,深入理解所有的概念对于使用 Vuex 来说是必要的。

让我们开始吧。

State

包含所有应用级别状态的对象,简单说,就是这个对象里存储了整个应用的状态数据,也就是我们要传递使用的数据。
使用state,需要一个容器,定义容器:

1
2
3
var store = new Vuex.Store({
...
});

state对象就是放在这个容器里,不仅是state,上面介绍的五个核心概念,分别对应五个对象,它们都是放在这个容器里的。最重要的,我们要把这个容器注入根实例里,这样所有的子组件才可以使用。注入根实例:

1
2
3
4
new Vue({
// 把 store 对象提供给 “store” 选项,这可以把 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
25
26
27
28
29
30
31
32
33
34
35
36
<div id="app">
<counter></counter>
</div>

<script src="./vue.js"></script>
<script src="./vuex.js"></script>

<script>

//定义子组件
var counter = {
template:`<div>Number:{{ count }}</div>`,
computed:{
count(){
return this.$store.state.count;
}
}
}

//容器
var store = new Vuex.Store({
state:{
count: 100
}
});

//根实例
new Vue({
el:'#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components:{
counter //注册子组件
}
});
</script>

上面例子中,state中定义了一个状态,即count,若想获取这个状态,需要在子组件的计算属性中,通过this.$store.state来返回,然后才能反应到试图中。state中可以定义多个状态:

1
2
3
4
state:{
count: 100,
name:'Jack'
}

如果这俩个状态都需获取,那么在子组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var counter = {
template:`
<div>
Number:{{ count }}
Name:{{name}}
</div>
`,
computed:{
count(){
return this.$store.state.count;
},
name(){
return this.$store.state.name;
}
}
}

总结:
state对象用来存储整个应用的状态数据;
当需要用到state中状态时,需要使用this.$store.state来获取,哪个组件需要,就在那个组件的计算属性中获取。

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,就是说若需要改变state中的状态,不能简单的诸如count++这样操作,唯一的方法是在mutations中更改状态。
mutationsstate一样,需要写在容器里。

例子

我们在上面的例子中增加一个按钮,每次点击时会增加 count数值,这就需要用到mutations

1
2
3
4
5
<div id="app">
//增加按钮,绑定add事件
<a href="javascript:;" @click='add'>点击增加</a>
<counter></counter>
</div>
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:10
},
mutations:{
increment(state){
state.count++;
}
}
});

//在根实例中添加方法
methods:{
add(){
this.$store.commit('increment');
}
}

这个例子中,在mutations中定义方法increment,通过传参state获取到state中的状态,并实行更改操作,当点击按钮时,触发add事件,通过this.$store.commit提交更改,使视图更新。

提交载荷(Payload)

increment方法中可以添加参数,this.$store.commit中对应提交参数,即 mutation 的 载荷(payload)。比如,现在我需要点击按钮时,count数值增加10,可以通过参数来实现:

1
2
3
4
5
mutations:{
increment(state,num){
state.count += num; //传一个参数
}
}

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

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

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

1
2
3
4
5
6
7
methods:{
add(){
this.$store.commit('increment',{
num:10
});
}
}

对象风格的提交方式

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

1
2
3
4
this.$store.commit({
type:'increment',
num:10
});

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

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

Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。

总结:
mutations用于更改state中状态,是唯一可以更改状态的事件回调函数;
this.$store.commit(函数名,载荷)中提交mutation;
对象风格提交,this.$store.commit({type:函数名,参数名:参数值})
Mutation 必须是同步函数。

Action

Action 类似于 mutation,不同在于:

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

例子

注册一个action:

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,提交之后,通过 store.dispatch方法触发,即在add中:

1
2
3
4
5
6
7
8
9
methods:{
add(){
//直接提交mutation改变状态时
// this.$store.commit('increment',{num:10});

//通过action提交mutation,进而改变状态
this.$store.dispatch('incrementAction');
}
}

对比一下,发现使用action有点多次一举,确实,大部分情况我们是不需要用action来分发的,但是重要的一点是mutation 必须同步执行,如果想执行异步操作,就得使用action:

1
2
3
4
5
6
7
actions:{
incrementAction(context){
setTimeout(()=>{
context.commit('increment',{num:10});
},1000)
}
}

关于context,它类似store,但是不是store,打印一下context,结果:image
发现它包含了commit等几个方法,这样在实践中,如果需要多次调用commit,我们可以通过ES6的参数解构,来简化context.commit,即:

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

总结:
Action类似于mutation,但不是直接变更状态,是通过提交mutation
通过this.$store.dispatch(函数名)来触发;
Action可以包含任意异步操作,mutation必须是同步的;
context不是store,它包含commit等多个方法,可以通过ES6参数解构,简化context.commit

Getter

有时候我们需要从 store 中的 state 中派生出一些状态,假设还是上面的计数程序,现在增加一个count2,它与count的区别是增加到30后就不再增大,这时就可以用到Getter:

注册Getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//容器
const store = new Vuex.Store({
state:{
count:10
},
getters:{
filterCount(state){
return state.count > 30 ? 30 : state.count;
}
},
mutations:{
increment(state,payload){
state.count += payload.num;
}
},
actions:{
incrementAction({commit}){
setTimeout(()=>{
commit('increment',{num:10});
},1000)
}
}
});

getters中定义方法filterCount,通过参数state得到state中的状态,实行过滤。当我们需要用的时候,在子组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const counter = {
template:`
<div>
{{ count }}
{{ count2 }}
</div>
`,
computed:{
count(){
return this.$store.state.count
},
count2(){
return this.$store.getters.filterCount;
}
}

}

可以看到,在计算属性中,多添加了一个count2,返回的数据是从store.getters中获取的,不同于count是从store.state中获取的,这样其实很清晰明了了。此时点击按钮,当数值大于30后,count2便不会再增加了。

总结:
getter可以认为是store的计算属性,就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算;
getter没有更改状态;
getters中的数据要在store.getters中获取。

Module

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

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//模块A
var moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
//模块B
var moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
//容器
var store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

获取状态时,对应模块取得

1
2
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态