状态管理模式
问题:当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏并产生以下问题:
- 多个视图依赖于同一状态
- 来自不同视图的行为需要变更同一状态
解决方法:
- 将数据以及操作数据的行为都定义在父组件;
- 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递)
上述的解决方法并未根本上解决问题,随着项目的复杂会暴露出以下问题:
- 传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
- 我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,为什么不把组件的共享状态抽取出来,以全局单例模式管理呢?通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这就是 Vuex 背后的基本思想。
Vuex
Vue 简介
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
使用 Vuex
中大型单页应用:Vuex
小型单页应用:store 模式
store 模式
安装
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
| 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
| 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
| 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
| 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
| 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
| 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); }, }, });
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
| 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({ 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
| 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.count++; }, }, getters: { sumWithRootCount(state, getters, rootState) { return state.count + rootState.count; }, }, 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基础入门√