Fork me on GitHub

状态管理与vuex

介绍

Vuex的设计思想

Vuex的设计思想

Vuex把所有组件的所有状态和数据放在同一个内存空间去管理,把它成为state,state的数据可以容易地映射到组件上,来渲染组件,当组件的数据需要发生变化的时候,组件可以通过dispatch一个action,actoin可以做一些异步操作(比如从后端请求数据),action会commit一个mutation(组件里面也可以commit一个mutation),mutation是一个唯一可以修改state的途径,其他任何方式去修改state都是非法的。

使用场景和用来解决哪些问题

我们要修改映射在组件上的数据,可以直接在组件内部进行操作;利用Vuex修改数据,要经过action、mutation,修改数据的路径反而会变长。那我们什么时候需要用到Vuex呢?

  • 可以解决复杂应用的组件数据共享。如果不打算开发大型单页应用,使用Vuex 可能是繁琐冗余的。如果您的应用够简单,最好不要使用Vuex。一个简单的 global event bus 就足够您所需了。但是,如果构建一个中大型单页应用,Vuex将会成为自然而然的选择。因为它可以解决多个组件之间的状态共享,如果一些组件是兄弟组件甚至时关联度很低的组件,我们要共享他们的数据就比较困难。
  • 可以解决路由间的复杂数据传递,当遇到路由跳转,场景需要传递的参数很复杂的时候,用Vuex传递数据就比较好。

基本用法

安装和配置

首先通过npm安装Vuex:
npm install --save vuex

main.js里,通过Vue.use()使用Vuex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
import App from './app.vue';
Vue.use(VueRouter);
Vue.use(Vuex);
// 路由配置省略
const store = new Vuex.Store({
// vuex的配置
});
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

state

仓库store包含了应用的数据(状态)和操作过程。Vuex里的数据都是响应式的,任何组件使用同一store的数据时,只要store的数据变化,对应的组件也会立即更新。

数据保存在Vuex选项的state字段内,比如:

1
2
3
4
5
const store = new Vuex.Store({
state: {
count: 0
}
});

在任何组件中,可以直接通过$store.state.count来读取:

1
2
3
4
5
<template>
<div>
{{$store.state.count}}
</div>
</template>

直接写在template里显得有点乱,可以用一个计算属性来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{count}}
</div>
</template>
<script>
export default {
computed: {
count() {
return $store.state.count;
}
}
}
</script>

现在组件中的计数0已经可以显示出来了。

mutations

在组件内,来自store的数据只能读取,不能手动改变,改变store中数据的唯一途径就是显示地提交mutations。

mutations是Vuex的第二个选项,用来直接修改state里面的数据:

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

在组件中,通过this.$store.commit方法来执行mutations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
{{count}}
<button @click="handleIncreasement">+1</button>
<button @click="handleDecrease">-1</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return $store.state.count;
}
},
methods: {
handleIncreasement () {
this.$store.commit('increment');
},
handleDecrease () {
this.$store.commit('decrease');
}
}
}
</script>

组件只负责提交一个事件名,Vuex对应的mutations来完成业务逻辑。

mutations还可以接受第二个参数,可以是数字、字符串或对象等类型。

1
2
3
4
5
6
// main.js省略部分代码
mutations: {
increment (state, n = 1) {
state.count += n;
}
}
1
2
3
4
5
6
7
8
<script>
export default {
methods: {
handleIncreasementMore () {
this.$store.commit('increment', 5);
}
}
</script>

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

1
2
3
4
5
6
// main.js
mutations: {
increment (state, params) {
state.count += params.count;
}
}
1
2
3
4
5
// index.vue
this.$store.commit({
type: 'increment',
count: 10
})

注意:mutation里不要异步操作数据。如果异步操作数据了,组件在commit后,数据不能立即改变,而且不知道什么时候会改变。可以在action里处理异步,后面再讲。

在写mutation的时候,一个state内的属性对应一个mutation,也就是要体现出mutation的原子性,可以提高它的复用性,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mutations = {
setId(state, id) {
state.course_id = id;
},
setInfo(state, info) {
state.info = info;
},
setComments(state, comments) {
state.comments = state.comments.concat(comments);
},
setHotComments(state, hot_comments) {
state.hot_comments = state.hot_comments.concat(hot_comments);
}
};

高级用法

getters

有这样的场景:Vuex定义了某个数据list,它是一个数组:

1
2
3
4
5
6
// main.js
const store = new Vuex.Store({
state: {
list: [1, 5, 8, 10, 30, 50]
}
})

如果只想得到小于10的数据,可以在组件的计算属性里进行过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{ list }}
</div>
</template>
<script>
export default {
computed: {
list() {
return $store.state.list.filter(item => item < 10);
}
}
}
</script>

这样写完全没有问题。但如果还有其他的组建也需要过滤后的数据时,就得把computed的代码完全复制一份,而且需要修改过滤的方法时,每个用到的组件都得修改,这明显不是我们期望的结果,如果将computed的方法也提取出来就好了,getters就是来做这件事的:

1
2
3
4
5
6
7
8
9
10
// main.js
const store = new Vuex.Store({
state: {
list: [1, 5, 8, 10, 30, 50]
},
getters: {
filteredList: state => {
return $store.state.list.filter(item => item < 10);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
{{ list }}
</div>
</template>
<script>
export default {
computed: {
list() {
return $store.getters.filteredList;
}
}
}
</script>

getters也可以以来其他的getter,把getter时作为第二个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
const store = new Vuex.Store({
state: {
list: [1, 5, 8, 10, 30, 50]
},
getters: {
filteredList: state => {
return $store.state.list.filter(item => item < 10);
},
listCount: (state, getters) => {
return getters.filteredList.length;
}
}

在我看来,getters的作用就像是组件内的计算属性,用mutation单纯的为state内的属性赋值,保持mutation赋值的原始性,用getters作计算属性过滤。

actions

上面说过,mutation里不应该异步操作数据,所以有了action选项。action与mutation很像,不同的是action里面提交的是mutation,并且可以异步操作逻辑。

action在组件内通过$store.dispatch触发,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.js
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state, n = 1) {
state.count += n;
}
},
actions: {
increment (context) {
context.commit('increment');
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
{{ count }}
<button @click="handleActionIncrement">action +1</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
}
},
methods: {
handleActionIncrement () {
this.$store.dispatch('increment');
}
}
}
</script>

这样看来显得有些多此一举,但是加了异步就不一样了,我们可以用一个Promise在1秒后提交mutation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.js
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state, n = 1) {
state.count += n;
}
},
actions: {
asyncIncrement (context) {
return new Promise(resolve => {
setTimeout (() => {
contex.commit('increment');
resolve();
}, 1000)
});
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
{{ count }}
<button @click="handleAsyncIncrement">async +1</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
}
},
methods: {
handleAsyncIncrement () {
this.$store.dispatch('asyncIncrement').then(() => {
console.log(this.$store.state.count); // 1
});
}
}
}
</script>

Vuex使用的时候,涉及到改变数据的,就使用mutations,存在业务逻辑的,就用action。

上面也讲到,当需要异步获取网络数据的时候,一定得写在action中,得到了数据后才能提交mutation,起到改变state属性的效果。

实战中,或许我们会遇到一个问题:有多个网络请求,而且业务中需要我们做的是先请求到所有的数据,才能提交mutation对state中的属性进行“集体”的赋值,比如页面切换的时候,同时渲染出所有的数据模块会带来较好的体验感,另外要求我们的网络异步请求必须是同时发出的。

这里可以用到Promise.all对那几个异步网络请求进行封装,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const actions = {
fetchAll({ commit }, id) {
commit("setId", id);
Promise.all([
DetailService.getInfo(state.course_id),
DetailService.getComments(state.course_id, state.page),
DetailService.getHotComments(state.course_id)
]).then(value => {
preprocess(value[1]);
preprocess(value[2]);
commit("setInfo", value[0]);
commit("setComments", value[1]);
commit("setHotComments", value[2]);
});
}
}

Promise.all的作用就是当参数(一个promise数组)中所有的promise都resolve,它才resolve。看上面这个例子很明显了。hexo

modules

modules将store分割到不同的模块。当项目足够大时,store里的state、getters、mutations、actions会非常多,都放在main.js里显得不是很友好,使用modules可以把它们写到不同的文件中。每个modules拥有自己的state、getters、mutations、actions,而且可以多层嵌套:

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

参考资料

官方文档

undefined