目录
初识Vuex
什么是Vuex
为什么要用Vuex
Vuex的基本使用
计数器案例
在项目中使用Vuex
安装Vuex到项目中
引入vuex插件并注册
创建store仓库
实现全局组件可访问的store仓库
在组件中访问store仓库中的共享数据
管理共享数据
Vuex工作流程图简析
深入理解Vuex工作流程
组件实例如何触发dispatch
action方法定义
commit方法定义
mutation方法定义
整体流程过一遍
编辑
Vuex工作流程图的拓展知识
组件实例直接commit mutation
action和mutation的区别
在action中继续dispatch其他action
Vuex第四个核心:getters
Vuex辅助函数
Vuex中冗余的代码
mapState
mapState的作用
mapState的入参对象式写法
mapState的入参数组式写法
mapGetters
mapGetters的作用
mapGetters入参对象式写法
mapGetters入参数组式写法
mapActions
mapActions的作用
mapActions入参对象式写法
mapActions入参数组式写法
mapMutations
mapMutations的作用
mapMutations的入参对象式写法
mapMutations的入参数组式写法
最终改造后效果如下
Vuex模块化
什么是Vuex模块化
如何实现Vuex模块化
Vuex模块化项目结构划分
index.js的工作及注意事项
Vuex第五大核心配置modules
Vuex辅助函数的模块化适配
如何实现store.state的初始化以及持久化
基于Vuex工作流程来实现store.state的初始化和持久化
Vuex第六大核心 plugins
区分Vuex插件和Vuex的插件
Vuex的插件函数的入参以及执行时机
Vuex的插件函数中初始化state的两种方式
store.subscribe方法
案例代码分享
Vuex是基于Vue开发的插件,Vuex可以集中保存和管理多个组件共享的数据。
我们已经了解了Vuex的功能是:集中保存和管理多个组件共享的数据。
那么在没有Vuex之前,我们是如何保存和管理多个组件共享数据的呢?
Vue2.x - TodoList案例_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/125193079?spm=1001.2014.3001.5502在前面做的TodoList案例中,MyHeader、MyContent、MyFooter共享一个数据源,并且这些组件都需要对数据源数据进行操作。
这样一来,App组件就不仅是一个管理员组件,而且还是存放共享数据的仓库,另外还要定义和暴露各种操作共享数据的方法。但是,本质上来说,App组件并不需要使用这些共享数据。
如果项目过大,则App管理的组件就越多,此时就出现了如下问题:
综上所述,App组件作为共享数据的仓库适合小型项目,对于大型项目而言,App组件并不适合保存和管理共享数据。
而Vuex是一个独立于组件之外,可以被所有组件访问到的保存和管理共享数据的仓库。
计数器是一个很适合学习Vuex的案例,下面是未使用Vuex的计数器实现:
演示效果:
下面我们将一边学习Vuex,一边改造计数器案例。
(注意:vue2对应vuex3,vue3对应vuex4)
npm i vuex@3
Vuex插件的目的是创建一个存放和管理共享数据的仓库,而在引入的Vuex对象上有一个Store构造函数,该构造函数可以创建一个store实例,store实例就是存放和管理共享数据的仓库,共享数据存放在创建store实例的Vuex.Store的入参配置对象的state属性中。比如计数器案例中,count值就可以看出一个共享数据。
现在我们已经有了store仓库,接下来就是需要让所有组件实例都能访问到这个store仓库。Vuex的实现方案是:在Vue构造函数入参配置对象中扩展一个配置属性store,该配置属性接收一个Vuex.Store实例,一旦该配置被解析,则vm管理的所有组件实例上都会挂载一个$store属性,值就是new Vue时传入的options.store值。
new Vuex.Store => (vm.)options.store => (vc.)$store
而在组件实例的$store属性对象上可以访问到 store仓库实例的state属性,这意味着所有组件实例都可以访问到共享数据了
所以计数器案例中,我们可以将共享数据count从App组件中转移到Vuex仓库中定义,然后App组件也支持直接从自身$store.state上获取到count
至此我们已经实现了:将共享数据存放到基于Vuex实现的store仓库中,并且在任意组件中访问到store仓库中的共享数据。
我们知道Vuex的功能是保存和管理多个组件共享的数据。目前保存已经实现了,但是如何管理呢?
上面图示,就是Vuex的工作流程,首先我们对其中各个要素进行解释:
从图中虚线可以看出,Vuex是由三大核心组成的:Actions、Mutations、State。
Actions、Mutations、State其实都是Vuex.Store构造函数入参配置对象的属性,即它们都是创建store仓库实例的重要配置。
Actions对象中存放了各种action方法,Mutations对象中存放了各种mutation方法。
从图示中可以看出,action方法的职责有三个:
而mutation方法的的职责有三个:
State对象是保存具体共享数据的地方,是实际意义的仓库。另外,我们需要注意的是:
state对象中保存的共享数据都是响应式的
它们的响应式逻辑和组件的data属性响应式原理一致,只要数据发生改变,使用数据的组件的模板就会被重新解析渲染。这也就是流程图中,State对象的render线的工作。
通过上面对于Vuex三大核心的工作内容解释,我们可以隐约知道:actions和mutations就是Vuex管理共享数据的方式。
目前我们在组件中已经可以通过$store.state.count访问到共享数据了,那么如何操作共享数据呢?按照流程图看,组件实例需要触发一个dispatch,那么dispatch是啥,又在哪呢?
dispatch是一个Vuex.Store.prototype上的方法
所以只要有store实例,就可以访问到disptch方法,而组件实例上的$store属性就是一个store实例。
dispatch方法有两个入参:
其实定义在actions中的action方法,类似于事件绑定的回调函数,action方法名就是事件类型。
而组件实例调用dispatch方法就是,触发指定事件类型(type),并传入数据(payload)给事件回调(action方法)。
action方法有两个入参:
action方法是Vuex底层自动调用的,所以context入参和payload入参也是自动传的,其中payload没有什么问题,而context是什么?又来用干什么工作呢?
context将action方法中可能用到的东西都准备好了,所以称为它为执行上下文。
commit方法可以提交action方法的处理结果到mutation方法中,commit方法入参有两个:
为了区别action和mutation方法,通常将mutation方法名定义为action方法名的全大写形式,如果action方法名由多个单词组成,则多个单词间由"_"连接。
mutation方法的入参有两个:
mutation方法的作用是将commit提交的处理结果保存到$store.state中。
dipatch => action => commit => mutation
当mutation完成state的数据修改后,就会触发响应式行为,引起使用该数据的组件模板的重新渲染。
按照Vuex工作流程图,组件实例必须通过dispatch分发数据给action,然后由action提交数据给mutation。
但是我们发现某些action其实只是做了一个数据透传的工作,这意味着action的定义变得冗余。
此时组件实例可以直接commit数据给mutation,即跳过action。
action和mutation都是用于管理共享数据的,二者的区别主要在于:
将异步操作放到mutation中,将导致Vue DevTools中记录的mutation动作的state结果和实际结果不一致。
直接在action中修改state,即不经过mutation,将导致Vue DevTools无法记录state的修改。
从上面两个测试结果来看,在action中修改state、在mutation中进行异步操作对于实际效果无影响,影响的只是Vue DevTools中的mutation记录。
我们可以在Vuex.Store入参配置对象中新增一个属性strict,设为true,表示严格按照 action中不能修改state,mutation中不能进行异步操作的标准进行检查,若不符合标准,则报错。
这里state的修改是在action中进行的,所以报错state不能在mutation以外执行。
这里延迟加也是报错 state不能在mutation之外执行,因为state的修改是在setTimeout的异步回调 ()=>{} 中执行的。而不是在INCREMENT_ASYNC中执行的。
action方法的入参context上,除了有state、commit外,还有dispatch,这意味着action方法中可以继续dispatch其他action。
实际业务场景中,我们经常会遇到一些复杂功能的业务,此时将整个复杂功能定义在一个action中,不仅降低了代码可读性,可维护性,而且一些可复用的功能也无法复用。所以我们需要将复杂功能分解为多个独立的简单功能,然后将每个简单功能定义为一个action,然后依次在action中dispatch到下一个action,像一个链条一样。
现在我们需要让计数器新增展示一个count * 10的值。
按照一般的思路,我们直接在组件内新增一个computed计算属性即可。
那么,如果这个count * 10值如果也是一个共享数据呢?此时需要在store.state中新定义一个属性吗?
答案是不需要,在store中还有一个配置属性getters,它相当于共享数据的计算属性。
组件实例需要通过 $store.getters.count10访问
在模板语法中访问store仓库中的共享数据,则必须要带$store.state或$store.getters前缀,形式上很冗余,此时我们可以使用计算属性来取出模板语法中的代码冗余
但是此时冗余度来到了计算属性中。这些代码形式非常公式化。
另外,dispatch和commit方法调用也是非常公式化的。
Vuex提供了四个辅助函数来帮助开发者自动生成上述公式化的代码。
由于将 store.state.xxx 投射为 组件内 计算属性 yyy 的形式过于固定。如:
yyy(){
return this.$store.state.xxx
}
其中yyy,xxx为动态的,其他的都是静态的。所以Vuex提供了mapState方法用于生成组件内计算属性函数,我们只需要给mapState提供xxx,yyy信息即可。
mapState可以接收一个对象,对象中可以定义多个属性:
mapState函数的返回值是一个对象,该返回值对象包含自动生成的(组件内)计算属性方法。
其实mapState的实现自动生成计算属性逻辑很简单,大致如下:
function mapState(options) {
const result = {}
for(let yyy in options) { // yyy是组件内计算属性名字
const xxx = options[yyy] // xxx是store.state仓库中共享数据属性的名字
const mappedState = function(){ // 自动生成的计算属性函数
return this.$store.state[xxx]
}
result[yyy] = mappedState
}
return result
}
得到了mapState返回值对象后,我们只需要将mapState返回值对象展开,合并入组件的computed对象即可。
mapState入参对象式写法适用于:【组件内计算属性名字】和【store.state仓库内共享数据属性名字】不一样时使用。
如:...mapState({myCount: 'count'}) ,计算属性名字为myCount,仓库中共享数据属性名字为count
当【组件内计算属性名字】和【store.state仓库内共享数据属性名字】一样时,这种对象式写法也存在一点冗余:
...mapState({count: 'count'})
此时mapState还支持传入一个数组,数组元素就是【组件内计算属性名字】和【store.state仓库内共享数据属性名字】一样时的名字。
...mapState(['count'])
mapState的入参数组式写法 算是 对象式写法 的 简写形式。
实现也很简单:
function mapState(options) {
// 数组式 转为 对象式
if(Array.isArray(options)) {
const newOptions = {}
options.forEach(item => {
newOptions[item] = item
})
options = newOptions
}
// 对象式逻辑
const result = {}
for(let yyy in options) { // yyy是组件内计算属性名字
const xxx = options[yyy] // xxx是store.state仓库中共享数据属性的名字
const mappedState = function(){ // 自动生成的计算属性函数
return this.$store.state[xxx]
}
result[yyy] = mappedState
}
return result
}
由于将 store.getters.xxx 投射为 组件内 计算属性 yyy 的形式过于固定。如:
yyy(){
return this.$store.getters.xxx
}
其中yyy,xxx为动态的,其他的都是静态的。所以Vuex提供了mapGetters方法用于生成组件内计算属性函数,我们只需要给mapGetters提供xxx,yyy信息即可。
当【组件内计算属性名字】和【store.getters仓库内共享计算属性的名字】不一样时,使用mapGetters入参对象式写法
当【组件内计算属性名字】和【store.getters仓库内共享计算属性的名字】一样时,使用mapGetters入参数组式写法
由于组件实例调用dispatch方法分发action的形式过于固定,如
incrementOdd(){
this.$store.dispatch('incrementOdd', {num: this.num})
},
其中 incrementOdd,'incrementOdd'是动态的,{num:this.num}是动态的,其余都是固定的。
当我们需要独立生成函数时,函数内参数只能来源于函数入参,所以上述代码可以改造为:
incrementOdd(val){
this.$store.dispatch('incrementOdd', val)
},
则此时,只有incrementOdd,'incrementOdd'是动态的,其余都是静态的。
所以Vuex提供了mapActions来生成这段代码
yyy(val){
this.$store.dispatch(xxx, val)
},
yyy是组件内method方法名子,xxx是store.actions中action方法名字
mapActions入参对象式写法适用于:【组件内method方法名】 和 【store.actions中方法名】不一致时
mapActions({yyy:xxx})
需要注意的是,mapActions返回的对象中的method方法(即自动生成的methods方法),其入参将作为dispatch的第二个参数。
所以我们需要在使用自动生成的mtehods方法时,按需传入数据。如上例中,incrementOdd作为事件回调时,传入了对象{num}作为事件回调入参。
mapActions入参数组式写法适用于:【组件内method方法名】 和 【store.actions中方法名】一致时,该种写法算是对象式写法的简写形式。
由于组件实例调用commit方法提交mutation的形式过于固定,如
increment(){
this.$store.commit('INCREMENT', {num: this.num})
},
其中increment和“INCREMENT”是动态的,{num:this.num}也是动态的,其余是静态的。
当我们需要独立生成函数时,函数内参数只能来源于函数入参,所以上述代码可以改造为:
increment(val){
this.$store.commit('INCREMENT', val)
},
所以只有increment和“INCREMENT”是动态的,其余是静态的。
所以Vuex提供了mapMutations来生成这段代码
yyy(val){
this.$store.commit(xxx, val)
},
yyy是组件内method方法名字,xxx是store.mutations中mutation方法名字
mapMutations入参对象式写法适用于:【组件内method方法名】 和 【store.mutations中方法名】不一致时
需要注意的是,利用mapMutations自动生成的组件内method,需要给定入参。
mapMutations入参数组式写法适用于:【组件内method方法名】 和 【store.mutations中方法名】一致时,此种写法算是对象式写法的简写形式。
需要注意的是,利用mapMutations自动生成的组件内method,需要给定入参。
当前我们将Vuex仓库创建在了main.js中,并且所有业务的共享数据及管理共享数据的行为都定义了一起。我们可以将计数器案例和TodoList案例合并在一起。
虽然此时store仓库只有两个共享数据,及它们的管理行为,但是一眼看去也非常乱。
所以,我们需要将store仓库中数据及数据管理行为 按照 业务 进行解耦,定义到不同模块中,然后将不同模块引入到store仓库中,而这就是Vuex的模块化。
按照Vuex官方建议,我们需要在src目录下创建一个store文件夹用于存放Vuex仓库实现及模块化相关代码。
首先我们需要将store仓库地生成转移到 src/store/index.js中完成,然后再在 src/main.js 中导入 src/store/index.js 暴露出去的store实例。
需要注意的是,Vuex插件的注册,即Vue.use(Vuex)需要在new Vuex.Store之前执行,否则报错。
所以下面这种书写方式会报错:
有人可能认为将 import store from xxx 放到 Vue.use(Vuex)之后即可修复错误,其实不然,实际上依旧会报同样的错误,
原因是:
ES6的import是在静态编译时执行的,即在main.js尚未执行前,main.js中所有import就已经完成了和export接出接口对接,当main.js开始执行时,优先获取import接入接口变量,然后才运行其他非import语句。所以,import有提升作用。关于ES6模块化可以参考下:
随笔-深入理解ES6模块化(三)_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/121950458?spm=1001.2014.3001.5501或者阮一峰大神的ES6模块化教程说明:
Module 的语法 - ECMAScript 6入门 (ruanyifeng.com)https://es6.ruanyifeng.com/#docs/module
Vuex模块化旨在将原本多个业务数据源杂糅存在在一起的store,根据不同业务进行数据分离,分成多个不同模块,每个模块都有自己的actions、mutations、state、getters。
而src/store/modules文件夹就是用于收集模块文件的。
模块文件最终需要对外暴露一个包含actions、mutations、state、getters等的配置对象,暴露的配置对象最终被index.js引入,并以模块化的方式传给Vuex.Store入参配置对象的modules配置。
Vuex.Store入参配置对象的modules属性是Vuex的第五大核心,它的作用是:让我们访问store仓库中共享数据时,或者使用store仓库中管理共享数据的action或mutation行为时也要按照模块化方式进行。
举个简单的例子:
没有使用Vuex模块化前,我们在组件模板语法中访问共享数据,一般如下:
$store.state.count
而使用了Vuex模块化后,我们需要这样访问:
$store.state.countAbout.count
这里countAbout、todosAbout就是我们创建store实例时,传入Vuex.Store入参配置对象的modules属性对象的属性
同样地,不仅是state数据访问受到了模块化影响,其他actions、mutations、getters中方法的使用也一样收到模块的影响,调用action方法、mutation方法、getters属性时同样要添加countAbout、todosAbout前缀。
对于state属性的访问,只需要将以前的 $store.state.xxx 变为 $store.state.模块名.xxx 即可
对于action、mutation方法和getters方法的调用,则需要改变组件实例调用dispatch和commit方法传入type参数,如以前为 'CHECK_ALL_TODO' ,现在则要变为 '模块名/CHECK_ALL_TODO'
但是我们目前已经使用mapXxx辅助函数来生成对应state、action、mutation、getters的访问或调用代码,那么mapXxx是否也能适配模块化呢?
mapState、mapActions、mapMutations、mapGetters这些方法之前说明时,一般都是一个对象入参,或者一个数组入参。但是实际上,他们还有一个首位可选参数:命名空间。
mapState(namespace?: string, map: Array | Object): Object
mapActions(namespace?: string, map: Array | Object): Object
mapMutations(namespace?: string, map: Array | Object): Object
mapGetters(namespace?: string, map: Array | Object): Object
这个可选的namespace入参就是用于进行模块化适配的,mapXxx的入参namespace值为Vuex.Store入参配置对象的modules属性设置给模块的名称
但是想要使mapXxx的入参namespace值起作用,模块的暴露的配置对象的必须开启命名空间,即需要配置namespaced:true
此时mapXxx辅助函数就可以根据namespace入参值进行模块化适配后的自动生成行为了。
我们以mapState为例,实现下:
function mapState(namespace, options) {
let isModule = false
if(typeof namespace !== 'string' && typeof options === 'undefined') {
options = namespace
} else {
isModule = true
}
// 数组式 转为 对象式
if(Array.isArray(options)) {
const newOptions = {}
options.forEach(item => {
newOptions[item] = item
})
options = newOptions
}
// 对象式逻辑
const result = {}
for(let yyy in options) { // yyy是组件内计算属性名字
const xxx = options[yyy] // xxx是store.state仓库中共享数据属性的名字
const mappedState = function(){ // 自动生成的计算属性函数
if(isModule) {
return this.$store.state[namespace][xxx]
} else {
return this.$store.state[namespace][xxx]
}
}
result[yyy] = mappedState
}
return result
}
之前的TodoList案例中,所有的todo都是从浏览器的localStorage中读取的,最终也是保存在浏览器的localStorage中,实现过程是:
使用Vuex后,我们将todos保存在了store.state中,那么如何实现store.state的初始化,以及当store.state数据改变时将其持久化呢?
目前,store.state中的数据只能预置,即提前写死,并且一旦网页刷新,则store.state就会被重置。
我们知道 store.state的数据可以被 组件实例调用dispatch或commit,且最终通过mutation方法修改掉,所以我们只需要找到App组件,在其mounted钩子中 读取localStorage中todos数据并通过dispatch或commit传给store.state
并且在App组件中深度监听store.state.todos的变动,一旦改变,则取出 store.state.todos持久化到localStorage中
此时就能完成store.state.todos的初始化和持久化。
但是这种方式给App组件增加了负担,不值得推荐。
首先我们要区别下:【Vuex插件】 和 【Vuex的插件】
【Vuex插件】是指Vuex本身是一个基于Vue开发的插件,即Vuex是Vue的插件。
【Vuex的插件】是指Vuex插件的插件。
我们一般将【Vuex的插件】配置为 创建store实例的Vuex.Store入参配置对象的plugins数组的元素。
【Vuex的插件】是一个函数,该函数会接收到一个所在的store实例作为入参,且该函数的调用时机是store实例初始化完成后。
所以,在 【Vuex的插件】函数中我们可以完成store.state的初始化。
但是此时报错提示,我们不能在插件函数中修改store.state,那不完犊子了吗....
此时我们需要借助一个store实例方法
这个方法可以在mutation方法外替换整个store.state,而不会触发state只能在mutation中修改的报错。
这里先是利用JSON.parse(JSON.stringify)来进行了store.state对象的深度拷贝,然后基于拷贝对象初始化了todos,最后将拷贝对象替换掉原来的store.state。
又或者使用下面这种方式:即严格按照只能在mutation中修改state的标准来搞。
store实例还可以调用一个实例方法subscribe,该方法接收一个handler函数作为参数。
而handler函数:会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态state作为参数
由于handler函数的调用时机是在每个mutation完成后,即store.state改变后,相当于handler函数深度监听了store.state的变化,一旦store.state发生变化,则handler被调用。
所有我们可以在handler函数中完成store.state的持久化动作。
但是上面逻辑存在一个问题,即handler是监听整个state的深度变化,所以不管是state.todosAbout.todos的变动,还是state.countAbout.count的变动,都会触发handler的执行,这将会造成严重的性能问题。
此时,我们需要借助handler的mutation入参,该入参是一个对象,具有如下属性:
我们可以根据mutation.type的前缀来判断是哪个模块的state数据发生了改变,比如上面type值为“todosAbout/ADD_TODO”,则说明是todosAbout模块的state发生改变。
此时handler引发的性能问题将得到缓解。但是仍然需要注意防抖和节流。
qwx427219/Todos_Count_Vuex: 基于Vuex的 Todo List & Count 整合案例 (github.com)https://github.com/qwx427219/Todos_Count_Vuex