什么是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
15import 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
6import store from xxx
new Vue({
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store
})
不管是否使用脚手架,Vuex的使用方法是一样的。接下来,就用具体的例子来介绍Vuex的使用。
不过在这之前,我们需要知道五个Vue的核心概念,它们分别是:
- State
- Mutation
- Action
- Getter
- Module
我们学习Vuex的使用,就是基于这五个概念的,深入理解所有的概念对于使用 Vuex 来说是必要的。
让我们开始吧。
State
包含所有应用级别状态的对象,简单说,就是这个对象里存储了整个应用的状态数据,也就是我们要传递使用的数据。
使用state,需要一个容器,定义容器:1
2
3var store = new Vuex.Store({
...
});
state对象就是放在这个容器里,不仅是state,上面介绍的五个核心概念,分别对应五个对象,它们都是放在这个容器里的。最重要的,我们要把这个容器注入根实例里,这样所有的子组件才可以使用。注入根实例:1
2
3
4new Vue({
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store
})
注意:一个页面只能有一个容器
例子
1 | <div id="app"> |
上面例子中,state中定义了一个状态,即count,若想获取这个状态,需要在子组件的计算属性中,通过this.$store.state
来返回,然后才能反应到试图中。state中可以定义多个状态:1
2
3
4state:{
count: 100,
name:'Jack'
}
如果这俩个状态都需获取,那么在子组件中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var 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中更改状态。mutations
跟state
一样,需要写在容器里。
例子
我们在上面的例子中增加一个按钮,每次点击时会增加 count
数值,这就需要用到mutations
1 | <div id="app"> |
1 | //容器 |
这个例子中,在mutations中定义方法increment
,通过传参state获取到state中的状态,并实行更改操作,当点击按钮时,触发add
事件,通过this.$store.commit
提交更改,使视图更新。
提交载荷(Payload)
即increment
方法中可以添加参数,this.$store.commit
中对应提交参数,即 mutation 的 载荷(payload)。比如,现在我需要点击按钮时,count
数值增加10,可以通过参数来实现:1
2
3
4
5mutations:{
increment(state,num){
state.count += num; //传一个参数
}
}
1 | methods:{ |
但在大多数情况下,参数应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:1
2
3
4
5mutations:{
increment(state,payload){
state.count += payload.num;
}
}
1 | methods:{ |
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:1
2
3
4this.$store.commit({
type:'increment',
num:10
});
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 mutations 保持不变:1
2
3
4
5mutations:{
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
9methods:{
add(){
//直接提交mutation改变状态时
// this.$store.commit('increment',{num:10});
//通过action提交mutation,进而改变状态
this.$store.dispatch('incrementAction');
}
}
对比一下,发现使用action有点多次一举,确实,大部分情况我们是不需要用action来分发的,但是重要的一点是mutation 必须同步执行,如果想执行异步操作,就得使用action:1
2
3
4
5
6
7actions:{
incrementAction(context){
setTimeout(()=>{
context.commit('increment',{num:10});
},1000)
}
}
关于context,它类似store,但是不是store,打印一下context,结果:
发现它包含了commit等几个方法,这样在实践中,如果需要多次调用commit,我们可以通过ES6的参数解构,来简化context.commit
,即:1
2
3
4
5
6
7actions:{
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:
注册Getter1
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
17const 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
2store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态