Vuex

290 阅读2分钟

前言

  • 组件为什么要通信,其目的就是为了改变其对应的数据,Vue 是 mvvm 框架的视图又是基于数据去驱动的,而数据改变,带来的则是视图的改变
  • 那我们能不能把需要的一些多个组件都要访问的数据拿出来,放到全局里面进行管理呢?下面让我们一起学习 Vue 额全局管理模式 Vuex

是什么?

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

解决了什么问题?

  • 当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏
    • 多个视图依赖于同一状态
    • 来自不同视图的行为需要变更同一状态

好处

  • 我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为
  • 通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护

设计模型

  • 把组件的共享状态抽取出来,以一个全局单例模式管理
  • 你可以把它想象成一个数据集成管理的库

安装

npm install vuex

或者你可以在 vue create project 时选择 vuex 一并安装

初始化使用

  • src 目录下创建 store 文件夹,新建 index.js 入口文件 作为仓库

store/index.js

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

// 使用插件
Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        msg: 'hello world'
    }
});

export default store;
  • 引入 Vuex

main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
    store,
    render: h => h(App),
}).$mount('#app')
  • 这时,我们就可以在组件内通过 this.$store 看到这个实例了
<script>
	export default {
		mounted() {
			console.log(this.$store);
		},
	};
</script>

State

  • 单一状态树,用一个对象就包含了全部的应用层级状态
  • 他作为一个唯一数据源而存在,每个应用将仅仅只包含一个 store 实例
  • 单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照

在 Vue 组件中获得 Vuex 状态

  • 由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是使用计算属性
<template>
	<div id="app">
		{{ msg }}
		<button @click="handleMsg">change</button>
	</div>
</template>

<script>
	export default {
		computed: {
			msg() {
				return this.$store.state.msg;
			},
		},
		methods: {
			handleMsg() {
				this.$store.state.msg = "abc";
			},
		},
	};
</script>

mapState 辅助函数

  • 当我们一个组件需要获取多个状态时,this.$store.state[key] 这个声明很重复和冗余,又难看又烦
  • 为了解决这个问题,Vuex 整了一个辅助函数 mapState 帮助我们生成计算属性,让我们少打点代码
<template>
	<div id="app">
		<div>{{ msg1 }}</div>
		<div>{{ msg2 }}</div>
		<div>{{ msg3 }}</div>
		<button @click="handleMsg">change</button>
	</div>
</template>

<script>
	import { mapState } from "vuex";
	export default {
		data() {
			return {
				data: "data",
			};
		},
		// 使用 mapState 偷懒
		computed: mapState({
			// 写法一
			msg1: (state) => state.msg1,
			// 写法二
			msg2: "msg2",
			// 写法三:如需引用 this,使用常规函数
			msg3(state) {
				return state.msg3 + "---" + this.data;
			},
		}),
		methods: {
			handleMsg() {
				this.$store.state.msg1 = "change1";
				this.$store.state.msg2 = "change2";
				this.$store.state.msg3 = "change3";
			},
		},
	};
</script>
  • 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组
<script>
	import { mapState } from "vuex";
	export default {
		computed: mapState(["msg1", "msg2", "msg3"]),
	};
</script>

对象展开运算符

  • 如果向上面这种写法的话,计算属性就只能用于 state 了,那我们怎么能在偷懒的情况下正常使用计算属性?
  • mapState 函数返回的是一个对象,所以我们可以用一个工具函数把他包起来,而对象展开运算符可以让我们的写法大大简化
<script>
	import { mapState } from "vuex";
	export default {
		data() {
			return {
				data: "data",
			};
		},
		computed: {
			// 使用 展开运算符 简化写法
			...mapState(["msg1", "msg2"]),
			// mapState 是一个使写法简化的工具函数,可以写多个也没有问题
			...mapState({
				msg3(state) {
					return state.msg3 + "---" + this.data;
				},
			}),
			abc() {
				return this.data + 123456;
			},
		},
	};
</script>

Getter

  • 假设我们有一个场景,多个组件都需要依赖 store.state.msg 使用计算属性派生出一些状态,那我们不就需要在多个组件的计算属性都写上一样的代码?!这也未免太麻烦了
  • Vuex 上可以定义 getters 达到计算属性的效果。我们可以把他理解成 store 的计算属性
  • computed 一样,getters 也会被缓存起来

怎么使用

const store = new Vuex.Store({
    state: {
        msg: 'msg'
    },
    getters: {
        // Getter 接受 state 为第一参数
        getterMsg1(state) {
            return state.msg + ' --- getters'
        },
        // 如需依赖其他 getter 时,Getter 接受 getters 为第二参数
        getterMsg2(state, getters) {
            return state.msg + ' --- ' + getters.getterMsg1
        }
    }
});
store.getters.getterMsg1 // -> 'msg --- getters'

mapGetters 辅助函数

  • mapState 一样 Getter 也有辅助函数让我们简化书写
  • mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
<script>
	import { mapGetters } from "vuex";
	export default {
		computed: {
			...mapGetters(["getterMsg1"]),
			// 如果想将一个 getter 属性另取一个名字,使用对象形式
			...mapGetters({
				msg: "getterMsg2",
			}),
		},
	};
</script>

Mutation

  • State 示例中,我们改变 store 中的状态使用的是 this.$store.state.msg=newMsg 的方式,但实际上,这是不正确的,下面我们先看示例,再浅谈一下为什么
  • 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation

怎么使用

  • 我们先定义 mutations ,他非常像一个事件,每个 mutation 都有一个字符串的 事件类型(type)和一个回调函数(handle)
const store = new Vuex.Store({
    state: {
        msg: 'msg'
    },
    mutations: {
        // changeMsg 类似 事件类型(type)
        // 接收第一个参数 state
        changeMsg(state) {
            state.msg = 'newMsg'
        }
    }
});
  • 不能直接调用 mutation 的 handle,我们要通过 store.commit,这像是一个事件注册,当触发 changeMsg 类型的 commit 时,调用 mutation 的 handle
  • 你也可以理解成 commit 是传呼机,他会告诉 mutation 我被触发了,执行回调
this.$store.commit("changeMsg"); // 触发 changeMsg 类型的 commit

提交载荷(Payload)

  • commit 还允许携带额外的数据,即 mutation 的 载荷(payload)
const store = new Vuex.Store({
    state: {
        msg: 'msg'
    },
    mutations: {
        // payload 为 额外参数,payload不一定要传对象
        changeMsg(state, payload) {
            state.msg = payload.newMsg
        }
    }
});
this.$store.commit("changeMsg", { newMsg: "newMsg" });

对象风格的提交方式

  • 提交 mutation 的另一种方式是直接使用包含 type 属性的对象
this.$store.commit({
    // type 就是 mutation 的 key
    type: "changeMsg",
    newMsg: "newMsg",
});

为什么使用Mutation

  • 感觉使用 Mutation 好麻烦,先要定义 mutations,还要使用 commit 去进行提交,我直接 this.$store.state.msg=newMsg 他不香吗?别问!问就是官方规定!
  • 稍微皮一下,其实这么设计是有他的原因在的。我们设想一下,如果有多个组件都依赖同一个 store.state,他们都需要去改变这个状态,直接 this.$store.state.msg=newMsg,当结果不是我们预想的,我们怎么去找出问题出在哪?
  • 答案是否定的,多个地方直接修改对象,我们连在哪 debug 都不知道,想找出问题,谈何容易
  • 为了解决这个问题,Vuex 在外面包了一层函数,就是 mutation,需要改变 store.state 你找这一个函数。这样我们就可以解决 来源不清晰 的这个问题了
  • 那解决了来源的问题,为什么要得用 commit 进行触发?
  • mutations 内有多个我们定义的 handle,这样出现了两个问题
    1. 调用不统一
    2. 收集信息困难
  • 为此,Vuex 加多了一个中间层 commitmutations 内的 handle 都收集起来,给用户调用,而在用户触发的时候,commit 又会把信息收集起来
  • 正因如此,我们也可以在 DevTools 工具选择回溯 Vuex 的内容进行调试

Mutation 必须是同步函数

  • 一条重要的原则就是要记住 mutation 必须是同步函数
  • Mutation 是需要收集信息的,如果他是异步函数势必会使得收集信息产生混乱
  • 当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的
  • 需要异步可通过 Action

mapMutations辅助函数

  • 使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit
<template>
	<div id="app">
        <div>{{ msg }}</div>
		<!-- 同时支持载荷 -->
		<button @click="handleClick({ newMsg: 'newMsg' })">change</button>
	</div>
</template>
<script>
	import { mapMutations, mapState } from "vuex";
	export default {
		computed: mapState(["msg"]),
		methods: {
			// 将 this.changeMsg() 映射为 this.$store.commit('changeMsg')
			...mapMutations(["changeMsg"]),
			// 将 this.add() 映射为 this.$store.commit('increment')
			...mapMutations({
				handleClick: "changeMsg",
			}),
		},
	};
</script>

Action

  • Action 几乎跟 Mutation 一模一样,不同的点在于
    • 他是用户提交 Mutation ,而不会直接变更状态
    • 它可以进行任何的异步操作
  • 为什么会有 Action 呢?还记得刚才的 Mutation 吧,他已经有一套工作逻辑了,不过因为调用混乱,而不能进行异步操作
  • 这个时候 Vuex 在给他包了一层,就是 Action,异步在 Action,而他在异步操作回调的时候提交 Mutation,这样,就可以在不改变 Mutation 的情况下进行异步操作了,异步在 Action,提交去到 Mutation 时,又都保证了都是同步的
const store = new Vuex.Store({
    state: {
        msg: 'msg'
    },
    mutations: {
        changeMsg(state, payload) {
            state.msg = payload.newMsg
        }
    },
    actions: {
        asyncChangeMsg(context, payload) {
            // 接收参数 context,包含 commit、getters、state
            const { commit } = context;
            setTimeout(() => {
                // 记得 action 用于 提交 mutation
                commit("changeMsg")
            }, 1000);
        }
    }
});

触发

  • Action 通过 store.dispatch 方法触发
<script>
	import { mapState } from "vuex";
	export default {
		computed: mapState(["msg"]),
		methods: {
			handleClick() {
				// Action 支持 荷载
				// 写法一
				this.$store.dispatch("asyncChangeMsg", { newMsg: "newMsg" });
				// 写法二
				this.$store.dispatch("asyncChangeMsg", {
					newMsg: "newMsg",
				});
				// 写法三
				this.$store.dispatch({
					type: "asyncChangeMsg",
					newMsg: "newMsg",
				});
			},
		},
	};
</script>

mapActions辅助函数

  • Actions 也有对应的 mapActions 辅助函数
<script>
	import { mapActions, mapState } from "vuex";
	export default {
		computed: mapState(["msg"]),
		methods: {
			...mapActions(["asyncChangeMsg"]),
			...mapActions({
				handleClick: "asyncChangeMsg",
			}),
		},
	};
</script>

Module

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