一,前言
上一篇,主要介绍了 Vuex 中 Mutations 和 Actions 的实现,主要涉及以下几个点:
- 将 options 选项中定义的 mutation 方法绑定到 store 实例的 mutations 对象;
- 创建并实现 commit 方法(同步);
- 将 options 选项中定义的 action 方法绑定到 store 实例的 actions 对象;
- 创建并实现 dispatch 方法(异步);
至此,一个简易版的 Vuex 状态管理插件就完成了;
(尚不支持模块、命名空间、插件等高级能力及扩展能力)
本篇,继续介绍 Vuex 模块相关概念:Vuex 模块收集的实现;
二,Vuex 模块的概念
前面分别介绍了 vuex 中 state、getters、mutations、actions 的实现;
当项目庞大且状态复杂时,state、getters、mutations、actions 就会格外混乱甚至难以维护;
这时,我们希望能够将他们拆分为独立的模块(即作用域);每个模块为单独文件进行维护;
在每个独立的模块下,包含当前模块的全部状态:state、getters、mutations、actions;
定义模块,需要使用 Vuex 提供的 modules 属性;
当 Vuex 初始化时进行模块加载,会将 modules 中声明的多个模块与根模块进行合并;
Vuex 的模块,理论上是支持无限层级递归的模块树;
备注:
- 当多个模块中,存在相同名称的状态时,默认会同时变化;可添加 namespaced 命名空间进行隔离;
- 命名相同的模块会在模块收集阶段被覆盖;
三,Vuex 模块的使用
1,创建 Demo
基于之前示例代码的设计,为了完整地测试 vuex 的 modules 模块与 namespaced 命名空间功能;
仿造当前 Demo,另外再创建 2 个相似的模块:moduleA、moduleB:
模块 A:moduleA,修改 state.name = 20;
// src/store/moduleA
export default {
state: {
num: 20
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
}
};
模块 B:moduleB,修改 state.name = 30;
// src/store/moduleB
export default {
state: {
num: 30
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
}
};
在 src/store/index.js 中,引入并通过 modules 属性注册 moduleA 和 moduleB 两个模块:(即将 A、B 两个模块注册成为 index 根模块下的子模块)
// src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex'; // 使用 vuex 官方插件进行功能测试
// 引入两个测试模块
import moduleA from './moduleA'
import moduleB from './moduleB'
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
//...
},
getters: {
//...
},
mutations: {
//...
},
actions: {
//...
},
// 在根模块下注册子模块 A、B
modules:{
moduleA,
moduleB
}
});
export default store;
在 src/App.vue 中,测试模块方法的调用:
获取模块中的状态:this.$store.state.moduleA.num
;
备注:操作子模块时,需要添加对应的模块路径;
// src/App.vue
<template>
<div id="app">
商品数量: {{this.$store.state.num}} 个<br>
商品单价: 10 元<br>
订单金额: {{this.$store.getters.getPrice}} 元<br>
<button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>
<button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>
<!-- 测试 State 数据响应式 -->
<button @click="$store.state.num = 100">测试 State 数据响应式</button>
<br> 模块测试: <br>
A 模块-商品数量: {{this.$store.state.moduleA.num}} 个<br>
B 模块-商品数量: {{this.$store.state.moduleB.num}} 个<br>
</div>
</template>
npm run serve
启动服务,测试模块状态取值:
2,发现问题
测试状态更新:点击同步更新按钮
测试结果:
三个模块(根模块、模块 A、模块 B)中的三个同名状态 num 会同时发生改变,并触发了视图更新;
3,问题分析
从表象上看,虽然通过 modules 进行了模块拆分,但模块间的状态仍不是独立的;
备注:由于 demo 示例的特殊性,多个模块存在相同名称的属性:商品数量 num;
当 Vuex 初始化时,将进行模块合并,多个模块中的相同状态会被合并为一个数组;
之后,当通过同步更新方法 $store.commit('changeNum',5)
进行状态提交时,Vuex 会找到所有的changeNum
方法并依次执行,这就将导致同名属性一起更新;
为了使模块间的状态独立,即产生独立的作用域,需要通过 namespaced 命名空间 ,在状态合并阶段进行隔离;
4,命名空间
想要严格划分一个空间,需要为模块再添加 namespaced 命名空间:
// src/store/moduleA
export default {
namespaced: true, // 启动命名空间
state: {
num: 20
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
}
};
添加了 namespaced 命名空间后,再点击更新按钮,模块中的状态不会发生改变:
备注:这时,触发对应模块的数据更新需要添加模块的命名空间标识:
<template>
<div id="app">
<br> 模块测试: <br>
A 模块-商品数量: {{this.$store.state.moduleA.num}} 个<br>
B 模块-商品数量: {{this.$store.state.moduleB.num}} 个<br>
<button @click="$store.commit('moduleA/changeNum',5)">A 模块-同步更新:数量+5</button>
<button @click="$store.commit('moduleB/changeNum',5)">B 模块-同步更新:数量+5</button>
</div>
</template>
测试效果:
四,Vuex 模块收集的实现
1,模块的树型结构-模块树
文中第二部分提到:Vuex 的模块,理论上是支持无限层级递归的树型结构;
但当前版本的 Vuex 仅仅处理了单层 options 对象;
因此,需要继续添加支持深层处理的递归逻辑,从而完成“模块树”的构建,即:实现 Vuex 的模块收集;
为了能够模拟出多层级的“模块树”,我们再创建一个模块 ModuleC,并注册到 ModuleA 下;
模块层级设计:index 根模块中包含模块 A、模块 B;模块 A 中,又包含了模块 C;
模块 C:moduleC,修改 state.name = 40;
export default {
namespaced: true,
state: {
num: 40
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
}
};
在模块 A 中引入模块 C,并将模块 C 注册为模块 A 的子模块:
import moduleC from './moduleC'
export default {
namespaced: true,
state: {
num: 20
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
},
modules: {
moduleC
}
};
2,模块收集的逻辑
将 options 数据进行格式化,添加父子模块的层级关系,构建成为“模块树”;
创建 src/vuex/module/module-collection.js,用于对 options 进行格式化处理,即:通过递归地将子模块注册到对应的父模块上,完成“模块树”的构建;
备注:Vuex 的模块收集过程与 Vue 源码中 AST 语法树的构建过程相似:
- 首先,层级上都有父子关系,且理论上支持无限递归;
- 其次,都采用了深度优先遍历,需使用栈保存层级关系(这里的栈相当于地图的作用);
当 Vuex 模块收集完成后,我们期望的构建结果如下:
// 模块树对象
{
_raw: '根模块',
_children:{
moduleA:{
_raw:"模块A",
_children:{
moduleC:{
_raw:"模块C",
_children:{},
state:'模块C的状态'
}
},
state:'模块A的状态'
},
moduleB:{
_raw:"模块B",
_children:{},
state:'模块B的状态'
}
},
state:'根模块的状态'
}
3,模块收集的实现
在 src/vuex 目录下,创建 module 模块目录,并创建 module-collection.js 文件,创建 ModuleCollection 类:用于在 Vuex 初始化时进行模块收集操作;
/**
* 模块收集操作
* 处理用户传入的 options 选项
* 将子模块注册到对应的父模块上
*/
class ModuleCollection {
constructor(options) {
// ...
}
}
export default ModuleCollection;
模块收集的操作,就是(深度优先)递归地处理 options 选项中的 modules 模块,构建成为树型结构:
创建 register 方法:携带模块路径,对当前模块进行注册,执行“模块树”对象的构建逻辑;
class ModuleCollection {
constructor(options) {
// 从根模块开始,将子模块注册到父模块中
// 参数1数组:栈结构,用于存储路径,标识模块树的层级关系
this.register([], options);
}
/**
* 将子模块注册到父模块中
* @param {*} path 数组类型,当前待注册模块的完整路径
* @param {*} rootModule 当前待注册模块对象
*/
register(path, rootModule) {
// 格式化,并将当前模块,注册到对应的父模块上
}
}
export default ModuleCollection;
1,处理根模块
从根模块开始构建:格式化根模块,并初始化 newModule “模块树”对象 :
class ModuleCollection {
constructor(options) {
this.register([], options);
}
register(path, rootModule) {
// 格式化:构建 Module 对象
let newModule = {
_raw: rootModule, // 当前模块的完整对象
_children: {}, // 当前模块的子模块
state: rootModule.state // 当前模块的状态
}
// 根模块时:创建模块树的根对象
if (path.length == 0) {
this.root = newModule;
} else {
// 非根模块时:将当前模块,注册到对应父模块上
}
}
}
export default ModuleCollection;
若当前模块存在 modules 子模块,递归调用 register 方法(深度优先),继续注册子模块:
class ModuleCollection {
constructor(options) {
this.register([], options);
}
register(path, rootModule) {
let newModule = {
_raw: rootModule,
_children: {},
state: rootModule.state
}
if (path.length == 0) {
this.root = newModule;
} else {
// 非根模块时:将当前模块,注册到对应父模块上
}
// 若当前模块存在子模块,继续注册子模块
if (rootModule.modules) {
// 采用深度递归方式处理子模块
Object.keys(rootModule.modules).forEach(moduleName => {
let module = rootModule.modules[moduleName];
// 将子模块注册到对应的父模块上
// 1,path:待注册子模块的完整路径,当前父模块path拼接子模块名moduleName
// 2,module:当前待注册子模块对象
this.register(path.concat(moduleName), module)
});
}
}
}
export default ModuleCollection;
注意,调用 register 时,需要拼接好当前子模块的路径层级,便于确定层级关系时,快速查找父模块;
2,处理子模块
当 register 方法处理非根模块时,需要将当前模块,注册到对应父模块上;
这就需要从 root 模块树对象中,逐层地查找到当前模块的父模块对象,并将子模块添加进去:
class ModuleCollection {
constructor(options) {
this.register([], options);
}
register(path, rootModule) {
let newModule = {
_raw: rootModule,
_children: {},
state: rootModule.state
}
if (path.length == 0) {
this.root = newModule;
// 非根模块时:将当前模块,注册到对应父模块上
} else {
// 逐层找到当前模块的父亲(例如:path = [a,b,c,d])
let parent = path.slice(0, -1).reduce((memo, current) => {
//从根模块中找到a模块;从a模块中找到b模块;从b模块中找到c模块;结束返回c模块即为d模块的父亲
return memo._children[current];
}, this.root)
// 将d模块注册到c模块上
parent._children[path[path.length - 1]] = newModule
}
if (rootModule.modules) {
Object.keys(rootModule.modules).forEach(moduleName => {
let module = rootModule.modules[moduleName];
this.register(path.concat(moduleName), module)
});
}
}
}
export default ModuleCollection;
3,子模块注册逻辑
假设:模块层级深度为[a, b, c, d];
问:如何将模块 d 注册到它的父模块 c 上?
- 根据 register 方法中模块 d 携带的模块路径 path,即
path = [a, b, c, d]
; - 通过
path.slice(0, -1).reduce
逐层地从当前已经构建完成的 root 模块树对象上,找到模块d 的父模块,即模块 c; - 将子模块 d 格式化后,注册到父模块 c 上,这样就完成了 Vuex 的模块收集操作;
测试模块树的构建结果:
在根模块 root 对象中,包含两个子模块:模块 A 和模块 B;
其中,模块 A 包含一个子模块:模块 C;
构建结果与期望相符,模块树构建完成;
五,结尾
本篇,主要介绍了 vuex 模块收集是如何实现的,主要包括以下几点:
- Vuex 模块的概念;
- Vuex 模块和命名空间的使用;
- Vuex 模块收集的实现-构建“模块树”;
下一篇,继续介绍 Vuex 模块相关概念:Vuex 模块安装的实现;
维护日志
- 20211006
- 对部分描述进行调整,使模块收集思路及关键逻辑通俗易懂;