Vuex 基础入门

状态管理模式

问题:当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏并产生以下问题:

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

解决方法:

  • 将数据以及操作数据的行为都定义在父组件;
  • 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递)

上述的解决方法并未根本上解决问题,随着项目的复杂会暴露出以下问题:

  • 传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
  • 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,为什么不把组件的共享状态抽取出来,以全局单例模式管理呢?通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这就是 Vuex 背后的基本思想

Vuex

Vue 简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

使用 Vuex

中大型单页应用:Vuex
小型单页应用:store 模式

store 模式

安装

1
yarn add vuex
1
2
3
4
import Vue from 'vue'
import Vuex from 'vuex'
new Vuex.Store({})
Vue.use(Vuex)

es6-promise

Vuex 依赖 promise,浏览器不支持 promise,需要引入 es6-promise

1
yarn add es6-promise
1
import 'es6-promise/auto'

Store(仓库)

Vuex 应用的核心 Store,Store 包含应用中大部分的状态 state

Vuex 和 全局对象的不同

  • Vuex 的状态存储是响应式的
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation

基础示例

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
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
});

store.commit('increment'); // 触发状态变更
console.log(store.state.count); // 获取状态对象
// 组件中
this.$store.commit('increment')
console.log(this.$store.state.count)
// 或者在计算属性中
computed: {
count() {
return this.$store.state.count;
},
}

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");

Vuex 计数器应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment: state => state.count++,
decrement: state => state.count--
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
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
// App.vue
<template>
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
</template>

<script>
export default {
name: "App",
components: {},
computed: {
count() {
return this.$store.state.count;
},
},
methods: {
increment() {
this.$store.commit("increment");
},
decrement() {
this.$store.commit("decrement");
},
},
created: function() {
console.log(this.$store.state.count);
},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

state(状态)

组件中获取 Vuex 状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
// App.vue
import { mapState } from "vuex";
export default {
name: "App",
components: {},
computed: {
count () {
return this.$store.state.count
}
),
created: function() {
console.log(this.$store.state.count);
},
};
</script>

mapState

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
<script>
// App.vue
import { mapState } from "vuex";
export default {
name: "App",
components: {},
data() {
return {
localCount: 4,
};
},
methods: {
increment() {
this.$store.commit("increment");
},
decrement() {
this.$store.commit("decrement");
},
},
computed: mapState({
count: (state) => state.count,
countAlias: "count",
countPlusLocalState(state) {
return state.count + this.localCount;
},
}),
// computed: mapState([
// // 映射 this.count 为 store.state.count
// 'count'
// ]),
created: function() {
console.log(this.$store.state.count);
},
};
</script>

对象展开运算符

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
<script>
// App.vue
import { mapState } from "vuex";
export default {
name: "App",
components: {},
data() {
return {
localCount: 4,
};
},
methods: {
increment() {
this.$store.commit("increment");
},
decrement() {
this.$store.commit("decrement");
},
},
computed: {
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
count: "count",
}),
},
created: function() {
console.log(this.$store.state.count);
},
};
</script>

getter(state 计算属性)

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
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: "...", done: true },
{ id: 2, text: "...", done: false },
],
},
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);
},
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");

mapGetters

跟 mapState 用法类似

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
//  App.vue
<template>
<div id="app">Completed Todos: {{ doneTodosCount }}</div>
</template>

<script>
import { mapGetters } from "vuex";
export default {
name: "App",
components: {},
data() {
return {
localCount: 4,
};
},
methods: {
increment() {
this.$store.commit("increment");
},
decrement() {
this.$store.commit("decrement");
},
},
computed: {
...mapGetters(["doneTodosCount"]),
},
created: function() {
// 方法访问
console.log(this.$store.getters.getTodoById(1));
},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Mutation(处理数据)

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
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 1,
},
mutations: {
increment(state, payload) {
// 提交负荷
state.count += payload.amount;
},
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
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
//  App.vue
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment">+10</button>
</div>
</template>

<script>
import { mapState } from "vuex";
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
increment() {
// 提交负荷
this.$store.commit("increment", { amount: 10 });
// 对象风格
// this.$store.commit({type: 'increment', amount: 10 });
},
},
computed: {
...mapState(["count"]),
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

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
//  App.vue
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment({ amount: 10 })">+10</button>
</div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
...mapMutations(["increment"]),
},
computed: {
...mapState(["count"]),
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Mutation 需遵守 Vue 的响应规则

  • 在 store 中初始化好所有所需属性
  • 当需要在对象上添加新属性时
1
2
Vue.set(obj, 'newProp', 123)
state.obj = { ...state.obj, newProp: 123 }

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
37
38
39
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 1,
},
mutations: {
increment(state, payload) {
// 变更状态
state.count += payload.amount;
},
},
actions: {
incrementAsync(context,payload) {
setTimeout(() => {
// 载荷形式
context.commit("increment", payload);
}, 1000);
},
// incrementAsync({ commit }) {
// setTimeout(() => {
// commit("increment");
// }, 1000);
// },
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
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
//  App.vue
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment">+</button>
</div>
</template>

<script>
import { mapState } from "vuex";
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
increment() {
// 载荷形式
this.$store.dispatch("incrementAsync", { amount: 10 });
// this.$store.dispatch("incrementAsync");
},
},
computed: {
...mapState(["count"]),
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

mapActions

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
//  App.vue
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment">+</button>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
...mapActions(['incrementAsync']),
increment() {
this.incrementAsync({ amount: 10 })
},
},
computed: {
...mapState(["count"]),
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

组合 Action

结合 promise、async

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
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const store = new Vuex.Store({
state: {
count: 1,
},
mutations: {
increment(state, payload) {
// 变更状态
state.count += payload.amount;
},
},
actions: {
// incrementAsync(context, payload) {
// return new Promise((resolve) => {
// setTimeout(() => {
// context.commit("increment", payload);
// resolve();
// }, 1000);
// });
// },
incrementAsync({ commit }, payload) {
return new Promise((resolve) => {
setTimeout(() => {
commit("increment", payload);
resolve();
}, 1000);
});
},
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
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
//  App.vue
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment">+</button>
</div>
</template>

<script>
import { mapState, mapActions } from "vuex";
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
...mapActions(["incrementAsync"]),
// increment() {
// this.incrementAsync({ amount: 10 }).then(() => {
// setTimeout(() => {
// console.log(111);
// }, 1000);
// });
// },
async increment() {
await this.incrementAsync({ amount: 10 });
console.log(222);
setTimeout(() => {
console.log(111);
}, 1000);
},
},
computed: {
...mapState(["count"]),
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

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
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
60
//  main.js
import Vue from "vue";
import App from "./App.vue";
import "es6-promise/auto";
import Vuex from "vuex";

Vue.config.productionTip = false;

Vue.use(Vuex);

const moduleA = {
state: {
count: 3,
},
mutations: {
increment(state) {
// 这里的 `state` 对象是模块的局部状态
state.count++;
},
},
getters: {
sumWithRootCount(state, getters, rootState) {
return state.count + rootState.count; // rootState 为根节点状态
},
},
actions: {
incrementIfOddOnRootSum({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit("increment");
}
},
},
};

const moduleB = {
state: {
count: 8,
},
mutations: {},
getters: {},
actions: {},
};

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB,
},
state: {
count: 10,
},
});

new Vue({
store,
render: (h) => h(App),
}).$mount("#app");

console.log(store.state.a.count);
console.log(store.state.b.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
29
30
31
32
33
34
35
36
37
38
39
40
41
//  App.vue  
<template>
<div id="app">
Completed Todos: {{ count }}
<button @click="increment">+</button>
</div>
</template>

<script>
export default {
name: "App",
components: {},
data() {
return {};
},
methods: {
increment(){
// 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的
this.$store.dispatch("incrementIfOddOnRootSum");
}
},
computed: {
count() {
return this.$store.getters.sumWithRootCount
},
},
created: function() {},
};
</script>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>


相关资料
Vuex
store 模式
大白话理解Vuex
从头开始学习Vuex
Vuex 注入 Vue 生命周期的过程
[视频]Scrimba 上的 Vuex 课程
[视频]vuex基础入门

作者

Fallen-down

发布于

2020-06-14

更新于

2020-09-11

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.