Vuex 状态(数据)管理

1. 简介

组件中包含视图(模板template)、双向绑定的数据(data)、以及一些方法(methods),这3个都写在同一个组件(component)里面, 一般视图(View)触发方法动作(Actions),动作影响数据状态(State), 数据状态的改变又反应到视图(View)上来,这样在一个组件内就形成了一个闭环。即当前组件的视图使用当前组件的数据,当前组件的动作(方法)只修改当前组件的数据,总体来说只能自己跟自己玩,不能多个组件相互玩。

我们有这样两个需求:

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

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。

对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

为了解决以上问题,我们把组件的共享状态(data)抽取出来,以一个全局单例模式管理, 这样所有组件在任意时候都可以访问全局状态,当在任意组件中修改全局状态,所有引用全局状态的视图也会随之改变(响应式)。这就是Vuex的功能。

简单来说Vuex在一个工程中只能有一个全局实例对象(Store),也就是该实例对象是整个应用级别的,一个对象就包含了全部的应用层级状态。store被称为仓库,用于盛放全局状态的容器。任何组件都能访问全局对象的数据(State),任何组件都可以修改全局对象的数据。这就是我们平常说的设计模式中的“单例模式”。

Vuex 的思想

当我们在页面上点击一个按钮,它会处发(dispatch)一个action, action 随后会执行(commit)一个mutation, mutation 立即会改变state, state 改变以后,我们的页面会获取新数据,页面发生了变化。 Store 对象,包含了我们谈到的所有内容: action, state, mutation

vuex 状态管理核心

状态管理有5个核心,分别是 stategettermutationaction 以及 module

  • state:存储状态。也就是变量。
    state为单一状态树,在state中需要定义我们所需要管理的数组、对象、字符串等等,只有在这里定义了,在vue.js的组件中才能获取你定义的这个对象的状态

  • getters:派生状态。
    也就是set、get中的get,有两个可选参数:state、getters,分别可以获取state中的变量和其他的getters。外部调用方式:store.getters.personInfo()。就和vue的computed差不多;
    当我们需要从store的state中派生出一些状态,那么我们就需要使用getter,getter会接收state作为第一个参数,而且getter的返回值会根据它的依赖被缓存起来,只有getter中的依赖值(state中的某个需要派生状态的值)发生改变的时候才会被重新计算

  • mutations:提交状态修改。
    也就是set、get中的set,这是vuex中唯一修改state的方式,但不支持异步操作。
    第一个参数默认是state。
    外部调用方式:store.commit('mutations方法名', 值)。和vue中的methods类似。

  • actions:和mutations类似。不过actions支持异步操作。
    第一个参数默认是和store具有相同参数属性的对象, context。
    外部调用方式:store.dispatch('action方法名', '值')
    action可以提交mutation,在action中可以执行store.commit,而且action中可以有任何的异步操作。

  • modules:store的子模块,内容就相当于是store的一个实例。调用方式和前面介绍的相似,只是要加上当前子模块名,如:**store.a.getters.xxx()**。

2. Vuex HelloWorld

2.1 Vuex安装

1
cnpm install vuex --save

显式使用 Vuex 插件,一般写在 src/main.js 中,或者写在其它js中然后再在 main.js 中引入

1
2
3
4
import Vue from 'vue'  
import Vuex from 'vuex'

Vue.use(Vuex)

2.2 多组件共享全局状态示例

分别在 Foo.vueBar.vue 中改变全局属性 count 值,然后在 App.vue 中显示 count 修改后的值。

定义全局单例对象 src/store/store.js

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

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
},

mutations: {
increment (state, payload) {
state.count += payload.step
},
decrement: state => state.count--,
}
})

定义一个全局实例对象(Vuex.Store):

该对象的状态(state)就是全局属性, 类似于组件的data,每个组件都可以访问和修改属性。

可变的(mutations)类似于组件中的methods, mutations中的每个方法称作为 mutation handler,用来修改state中的值,方法的参数可以有两个(state, payload) state表示全局的单例对象,payload(载荷)也就是参数,调用时可以传递参数,该参数是可选的。

使用Mutation时需遵守的一些规则:

  • 最好提前在你的 store 中初始化好所有所需属性。
  • 当需要在对象上添加新属性时,你应该使用 Vue.set(obj, 'newProp', 123), 或者以新对象替换老对象
  • Mutation 必须是同步函数

src/main.js 中导入store.js并作为Vue的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'  
import App from './App'
import router from './router'
import store from './store/store'

Vue.config.productionTip = false


new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>'
})

将store作为Vue的选项,这样Vue会将store“注入”到每一个子组件中,也就是说每个组件都可以通过 this.$store 来访问全局单例对象store。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Foo.vue  
<template>
<div>
Foo.vue <button @click="increment">+</button>
</div>
</template>

<script>
export default {
name: 'Foo',
methods: {
increment () {
this.$store.commit('increment', {
step: 10
})
}
}
}
</script>

调用 store 中的 mutations 方法只能通过提交的方式 this.$store.commit('方法名', 负载参数) 这一种形式来调用,而不能使用 this.$store.方法名 这种普通的 对象.方法() 方式直接调用。如果感觉这种方式麻烦,Vuex提供了一种将Mutations映射(map)为methods的方式, 然后在使用时直接调用method就会自动帮你commit。

mapMutations() 函数,它接收一个参数,参数类型可以是数组也可以是对象:

  • 数组类型:当使用方法时方法名称和Mutation的名称一样时使用数组类型。
  • 对象类型:当使用方法时方法名称不想和Mutation的名称一样,可以对method起一个新的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>  
<div>
Foo.vue <button @click="add({step: 10})">+</button>
</div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
name: 'Foo',
methods: {
// 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// 将 `this.incrementBy({step: 10})` 映射为 `this.$store.commit('incrementBy', {step: 10})`
...mapMutations(['increment', 'incrementBy']),

// 将Mutation(increment)映射为method(add)
...mapMutations({
add: 'increment'
})
}
}
</script>

注意:mapMutations 只是将 Mutations 简单的映射为 methods, 其中 method 的方法体只包含 this.$store.commit('mutation', payload) 这一样代码,如果method还要处理其它业务逻辑的话,那么只能使用提交commit方式,而不能使用映射方式mapMutations。

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
// Bar.vue
<template>
<div>
Bar.vue <button @click="decrement">-</button>
</div>
</template>

<script>
export default {
name: 'Bar',
methods: {
decrement () {
this.$store.commit('decrement')
}
}
}
</script>

// App.vue
<template>
<div id="app">
App.vue {{count}}
<router-view name="foo"></router-view>
<router-view name="bar"></router-view>
</div>
</template>

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

可以通过 this.$store.state.count 来获取count的值,也可以将 this.$store.state.count 这行代码包装到计算属性count中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/router/index.js  
import Vue from 'vue'
import Router from 'vue-router'

import Foo from '../components/Foo'
import Bar from '../components/Bar'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
components: {
foo: Foo,
bar: Bar
}
}
]
})

为了在一个组件中使用多个兄弟组件,使用命名视图,将这些兄弟组件作为父组件的孩子组件。

2.3 actions

Action 类似于 mutation,Action 用于分发(dispatch) mutation,而不直接修改状态。 Action 可以包含任意异步操作(如发送http请求)

action 方法接收两个参数(context, payload),context为Context对象,context对象store实例具有相同方法和属性,所以context可以解构成 var {state, dispatch, commit, getters} = context

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
46
47
48
49
// src/store/store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
},

mutations: {
increment (state, payload) {
state.count += payload.step
},
decrement: state => state.count--,
},

actions: {
incrementAsync (context, payload) {
console.log(context.state)
console.log(context.getters)

// 延迟1秒执行
setTimeout(() => {
context.commit('increment', payload)
}, 1000)
}
}
})

// Foo.vue
<template>
<div>
Foo.vue <button @click="increment">+</button>
</div>
</template>

<script>
export default {
name: 'Foo',
methods: {
increment () {
this.$store.dispatch('incrementAsync', { step: 10 })
}
}
}
</script>

如果感觉封装个方法调用 store 的 dispatch() 方法麻烦的话可以使用 mapActions() 辅助函数,它的用法和 mapMutations 一样。mapActions() 的作用就是简单的将 this.$store.dispatch('Action名称', payload) 这一行代码封装到方法中。同样该函数接收一个参数,该参数的类型可以是数组类型也可以是对象类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>  
<div>
Foo.vue <button @click="add({ step: 10 })">+</button>
</div>
</template>

<script>
import { mapActions } from 'vuex'
export default {
name: 'Foo',
methods: {
// 参数为数组类型:method的名称和action名称一样
...mapActions(['incrementAsync']),
// 参数为对象类型:method的名称和action名称不一致
...mapActions({
add: 'incrementAsync'
})
}
}
</script>

Action中操作一般都是异步的,通常都需要在异步操作完成后做一些其它逻辑,如何知道异步处理完成了呢?可以在action中将异步处理的逻辑封装到Promise对象中,当异步逻辑处理完成就会调用Promise对象的then()方法,这样我们将异步完成后的逻辑写在then()方法中即可。

注意:dispatch(‘action’, payload)函数返回一个Promise对象,如果action中显式返回Promise, 那么dispatch()函数返回的就是action中的promise对象,如果action中没有显式的返回Promise对象,系统会将action中的逻辑封装到一个新的Promise对象中,然后返回一个新的promise对象,所以dispatch()之后可以调用then()

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// src/store/store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
},

mutations: {
increment (state, payload) {
state.count += payload.step
},
decrement: state => state.count--,
},

actions: {
incrementAsync (context, payload) {
console.log(context.state)
console.log(context.getters)

return new Promise((resolve, reject) => {
// 延迟1秒执行
setTimeout(() => {
// 提交mutation
context.commit('increment', payload)
// 成功,继续执行
resolve('异步执行结束')
}, 1000)
})
}
}
})

// Foo.vue

<template>
<div>
Foo.vue <button @click="increment">+</button>
</div>
</template>

<script>
export default {
name: 'Foo',
methods: {
increment () {
// dispatch()函数的返回值为action的返回值, action返回Promise
// 所以dispatch()之后可以调用then()
this.$store.dispatch('incrementAsync', { step: 10 }).then((resp) => {
console.log(resp)
})
}
}
}
</script>

action之间相互调用通过dispatch来调用

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

Vue.use(Vuex)

export default new Vuex.Store({
// ...
actions: {
fooAsync () {
console.log('fooAsync')
},
barAsync ({ dispatch }) {
// 使用dispatch调用其它action
dispatch('fooAsync').then(() => {
console.log('barAsync')
});
}
}
})

3. 组件之间传递参

3.1 src/store/store.js

预先定义出属性名 pageParams

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'  
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
pageParams: {}
}
})

3.2 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>  
<div id="app">
<button @click="goPage">页面传参</button>
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'App',
methods: {
goPage () {
this.$store.state.pageParams = {foo: 'foo', bar: 'bar'}
this.$router.push('/foo')
}
}
}
</script>

修改对象的某个属性除了使用等号=直接赋值外,也可以使用Vue的全局方法set来赋值。Vue.set(对象, ‘属性名’, 属性值)

1
2
import Vue from 'vue'  
Vue.set(this.$store.state, 'pageParams', {foo: 'foo', bar: 'bar'})

3.3 src/router/index.js

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

import Foo from '../components/Foo'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/foo',
component: Foo
}
]
})

3.4 Foo.vue

1
2
3
4
5
6
7
8
9
10
11
<template>  
<div>
{{ this.$store.state.pageParams }}
</div>
</template>

<script>
export default {
name: 'Foo'
}
</script>

3.5 mapState 辅助函数

当组件中要访问多个store中的状态就需要写多个计算属性,比较麻烦,可以使用mapState辅助函数简化写法, mapState函数的作用就是用于生成计算属性。

mapState函数接收一个对象参数或数组参数

  • 数组参数: 如果直接取store下的state值而不需要任何计算,可以直接传数组参数,值为store中的state中的属性名
  • 对象参数:如果需要对全局属性进行额外的计算,可以使用一个函数进行计算

mapState({ 计算方法 })

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>  
import { mapState } from 'vuex'
export default {
name: 'Foo',
computed: mapState({
pageParams1 (state) {
return state.pageParams
},
// 传字符串参数 'pageParams' 等同于 `state => state.pageParams`
pageParams2: 'pageParams'
})
}
</script>

mapState([‘属性名’])

1
2
3
4
5
6
7
<script>  
import { mapState } from 'vuex'
export default {
name: 'Foo',
computed: mapState(['pageParams'])
}
</script>

3.6 对象展开运算符 …mapState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>  
import { mapState } from 'vuex'
export default {
name: 'Foo',
data () {
return {
msg: 'Hello Vue'
}
},
computed: {
message () {
return this.msg + "!"
},
// 使用对象展开运算符将此对象混入到外部对象中,即computed对象中
// 相当于将mapState(['pageParams'])里的计算函数都拿到computed里,作为computed的函数
// 当计算函数中除了mapState外还有别的计算方法时使用
...mapState(['pageParams'])
}
}
</script>

4. Getter

Getter就是对store对象的状态state进行计算的方法,getter方法接收两个参数(state, getters), 可以通过this.$store.getters.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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// src/store/store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
todos: [
{ id: 1, text: 'Task A', isDone: true },
{ id: 2, text: 'Task B', isDone: false }
]
},
getters: {
doneTodos (state, getters) {
return state.todos.filter(item => item.isDone)
},

// 调用其他getter方法
doneTodosCount (state, getters) {
return getters.doneTodos.length
},

// getter方法也可以返回一个函数
getTodoById (state) {
var myfun = function (id) {
return state.todos.find(todo => todo.id === id)
}
return myfun;
}
}
})


// App.vue

<template>
<div id="app">
{{ this.$store.getters.doneTodos }} <br>
{{ this.$store.getters.doneTodosCount }} <br>
{{ this.$store.getters.getTodoById(2) }} <br>

{{ doneTodoList }}
</div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
name: 'App',
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters({
doneTodoList: 'doneTodos'
})
}
}
</script>

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性, mapGetters() 函数接收一个参数,参数类型可以是对象类型也可以是数组类型:

如果需要将getter方法映射成计算属性起一个别名时使用对象参数

如果不需要将getter方法映射成计算属性起一个别名时使用数组参数,数组里的值就是getter的名字,如 [‘doneTodos’, ‘doneTodosCount’]

5. module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

由于分割后会导致各个模块中state、mutation、action、getter会命名冲突,为了解决这个问题Vuex使用命名空间的概念来解决,就是类似于java中的package概念。

因module属于优化部分,暂时不详细说明。

6. 代码示例

state mutations getter,actions

  • state:对数据的全局存储
  • getter:可以理解为computed ,对数据进行计算
  • mutations:对数据的同步更改
  • actions:对数据的异步更改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mounted: {
console.log(this.$store);
/*payLoad 所有的参数对象 {a:aa,b:bb}*/
/*调用mutation方法*/
this.$store.commit("mutationsFun", payLoad)

/*调用action 方法*/
this.$store.dispatch("ActionsFun",payLoad)
},
computed: {
counter() {
return this.$store.state.count;
}
}

辅助函数

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
// 引入
import
{
mapState,
mapGetter,
mapMutation,
mapAction
}

//使用
[
computed: {
...mapState(['count']) /*映射同名的对象:count */
...mapState({
counter: 'count' /*映射不同名字的对象:count--->counter */
})
...mapState({
counter: (state) => state.count /*映射不同名字的对象:count--->counter */
})
...mapGetters(["fullName"])
}

methods:{
/*将actions,mutations里面的方法映射到对应的methods,以便在组件里面可以直接应用*/
...mapActions(['ActionsFuncName'])
...mapMutations(['MutationsFuncName'])
}
]

modules

  • modules 子state ,加入对应的作用域
  • 子模块中的方法参数state 作用域为子模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
modules:{
/*子模块的根目录 */
AModule: {
getters: {
textPlus(state, getters, rootState) {
return state.AModuleStateVar + rootStore.textVar + rootStore.BModule.StateVar /*可以取得root变量 可以去取其他模块 */
}
},
actions: {
add(state, commit, rootState) {
/*commit 默认在本模块中找对应的mutation */
/*设置root :true */
commit("mutationName", 参数对象, {
root: true
});
commit("OtherMudule/mutationName", 参数对象, {
root: true
})
}
}
}
}
  • 调用子module
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
mounted:
{
this.["AModuleName/ActionFunName"](参数对象)
this.["AModuleName/MutationFunName"](参数对象)
}
computed: {
textA() {
return this.$store.state.AModule.stateVar
}
...mapState({
counter: (state) => state.AModule.stateVar /*映射不同名字的对象:count--->counter */
})

...mapGetters(["AModuleName/GetterFunName"])
...mapGetters({
rootGetterFunName: 'rootGettterFunName',
AModuleGetterFunName: "AModuleName/GetterFunName"
})
}

methods: {
/*将actions,mutations里面的方法映射到对应的methods,以便在组件里面可以直接应用*/
...mapActions(['AModule/ActionsFuncName']) /*namespaced:true 需要调用namespace的名字*/
...mapActions([' AModuleNameActionsFuncName']) /*namespaced:false 需要调用namespace的名字*/
...mapMutations(['AModule_MutationsFuncName']) /*直接调用子模块的名字 */ /*namespaced:false 需要调用namespace的名字*/
/*子模块中的方法参数state 作用域为子模块 */
...mapMutations(['AModuleName/AModule_MutationsFuncName']) /*namespaced:true 需要调用namespace的名字*/
}

动态注册模块功能

1
2
3
4
5
6
7
import store from "./store";
/*等同于模块AModule*/
store.registerModule("CModule", {
state:...
action:...
})
store.unRegisterModule("CModule")

开发过程中store会更新里面的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
26
27
28
29
30
31
32
33
34
35
36
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'

Vue.use(Vuex)

const state = {
...
}

const store = new Vuex.Store({
state,
mutations,
modules: {
a: moduleA
}
})

if (module.hot) {
// 使 action 和 mutation 成为可热重载模块
module.hot.accept(['./mutations', './modules/a', ...], () => {
// 获取更新后的模块
// 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 加载新模块
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})
}

Store API

1
2
3
4
5
6
7
8
9
10
11
12
13
store.watch((state)=?state.count+1,(newCounter)=>{})

store.subscribe( (mutation,state)=>{
/*监听哪一个mutaion 被调用了*/
console.log(mutation.type)/*调用的是哪一个mutation function*/
console.log(mutation.payLoad)/*mutation 传入的参数*/
})

store.subscribeAction( (action,state)=>{
/*action 被调用了*/
console.log(action.type)/*调用的是哪一个action function*/
console.log(action.payLoad)/*action 传入的参数*/
})

Store Plugin

  • plugin 可以顺序执行,每一个plugin就是一个方法
1
2
3
4
5
6
new Vuex.Store({
strict: true,
plugins: [(store) => {
console.log("my plugin invorked")
}]
})

其他

1
2
3
new Vuex.Store({
strict:process.env.NoDE_ENV==='development'//production
})

参考


Vuex 状态(数据)管理
https://flepeng.github.io/021-frontend-04-Vue-Vuex-状态-数据-管理/
作者
Lepeng
发布于
2021年8月7日
许可协议