【Vuex 源码学习】第六篇 - Vuex 的模块收集
一,前言
上一篇,主要介绍了 Vuex 中 Mutations 和 Actions 的实现,主要涉及以下几个点:
将 options 选项中定义的 mutation 方法绑定到 store 实例的 mutations 对象;
创建并实现 commit 方法;
将 options 选项中定义的 action 方法绑定到 store 实例的 actions 对象;
创建并实现 dispatch 方法;
至此,一个简易版的 Vuex 状态管理插件就完成了;
本篇,继续介绍 Vuex 中模块相关概念,模块收集部分;
二,Vuex 模块的概念
前面分别介绍了 vuex 中 state、getters、mutations、actions 的实现;
当项目非常庞大时,state、getters、mutations、actions 中就会包含大量的内容;
这时,我们希望能够将他们拆分为独立的模块(即作用域);每个模块为单独文件进行维护;
在每个独立的模块下,包含当前模块的全部状态:state、getters、mutations、actions;
模块的定义,需要使用 Vuex 的 modules 属性;
当 Vuex 进行模块的加载时,会将 modules 中声明的多个模块与根模块进行合并;
当多个模块中,存在相同名称的状态时,默认会同时变化;可添加 namespaced 命名空间进行隔离;
Vuex 的模块,理论上是支持无限层级递归的模块树;
三,Vuex 模块的使用
创建 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 两个模块:
// 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: {
//...
},
modules:{
moduleA,
moduleB
}
});
export default store;
在 src/App.vue 中,测试模块方法的调用:
获取模块中的状态,需添加对应的状态路径:;
// src/App.vue
商品数量: {{this.$store.state.num}} 个
商品单价: 10 元
订单金额: {{this.$store.getters.getPrice}} 元
模块测试:
A 模块-商品数量: {{this.$store.state.moduleA.num}} 个
B 模块-商品数量: {{this.$store.state.moduleB.num}} 个
启动服务,测试模块状态取值:

发现问题
测试状态更新:点击同步更新按钮

根模块和 A、B 两个模块中,共三个 num 状态同时发生改变,并触发了视图更新;
问题分析
虽然划分了模块,但模块之间的状态却不是独立关系,
由于 demo 的特殊性,多个模块存在相同名称的属性:商品数量 num
当 Vuex 初始化时,将进行模块合并,多个模块中的相同状态会被合并为一个数组;
此时,当通过同步更新 进行状态提交时,就会找到所有方法依次执行,导致同名属性同时改变;
命名空间
如果想要严格划分一个空间,就需要再为模块添加一个命名空间 namespaced:
// src/store/moduleA
export default {
namespaced: true, // 启动命名空间
state: {
num: 20
},
getters: {
},
mutations: {
changeNum(state, payload) {
state.num += payload;
}
},
actions: {
}
};
添加了 namespaced 命名空间后,再点击更新按钮,模块中的状态不会发生改变:

这时,触发对应模块的数据更新需要添加模块的命名空间标识:
模块测试:
A 模块-商品数量: {{this.$store.state.moduleA.num}} 个
B 模块-商品数量: {{this.$store.state.moduleB.num}} 个
测试效果

四,Vuex 模块收集的实现
模块的树型结构-模块树
文中的第二部分提到:Vuex 的模块,理论上是支持无限层级递归的一棵树;
但是,在当前版本的 Vuex 中(自己实现的 Vuex 源码),只处理了一个 options 对象(单层);
因此,需要添加递归处理,完成“模块树”的构建,即:实现 Vuex 的模块收集;
为了模拟多层级的“模块树”,再创建一个模块 ModuleC;
模块层级:根模块中包含模块 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
}
};
模块收集的逻辑
将 options 数据进行格式化,添加父子模块的层级关系,构建称为“模块树”;
创建 src/vuex/module/module-collection.js,用于对 options 数据进行格式化处理;
备注:数据格式化,即构建“模块树”的逻辑,就是递归的将子模块注册到父模块中;
Vuex 模块收集的过程与 Vue 源码 AST 语法树的构建过程相似:
首先,层级上有父子关系,理论上是都支持无限递归的;
其次,由于采用深度优先的递归方式,需要使用栈结构来保存层级关系(相当于地图);
模块收集完成后,期望的构建结果如下:
// 模块树对象
{
_raw: '根模块',
_children:{
moduleA:{
_raw:"模块A",
_children:{
moduleC:{
_raw:"模块C",
_children:{},
state:'模块C的状态'
}
},
state:'模块A的状态'
},
moduleB:{
_raw:"模块B",
_children:{},
state:'模块B的状态'
}
},
state:'根模块的状态'
}
模块收集的实现
在 src/vuex 目录下,创建 module 模块目录,并创建 module-collection.js 文件;
创建 ModuleCollection 类:用于在 Vuex 初始化时进行模块收集操作;
/**
* 模块收集操作
* 处理用户传入的 options 选项
* 将子模块注册到对应的父模块上
*/
class ModuleCollection {
constructor(options) {
// ...
}
}
export default ModuleCollection;
模块收集的操作,就是(深度优先)递归地处理 options 选项中的模块,构建为树型结构:
class ModuleCollection {
constructor(options) {
// 从根模块开始,将子模块注册到父模块中
// 参数1数组:栈结构,用于存储路径,标识模块树的层级关系
this.register([], options);
}
/**
* 将子模块注册到父模块中
* @param {*} path 数组类型,当前待注册模块的完整路径
* @param {*} rootModule 当前待注册模块对象
*/
register(path, rootModule) {
// 格式化,并将当前模块,注册到对应父模块上
}
}
export default ModuleCollection;
从根模块开始构建:格式化根模块,并初始化模块树:
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 时,需要拼接好当前子模块的路径层级,便于确定层级关系时,快速查找父模块;
当 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;
备注:
假设,模块的层级深度为[a,b,c,d],如何将模块 d 注册到父模块 c 中?
根据模块 d 的路径 path,即 [a,b,c,d],以及当前已经构建完成的 root 模块树对象;
逐层地找到 [a,b,c,d] 中目标模块d 的父模块 c,对应在 root 模块树中的对象;
将子模块 d 格式化后,注册到父模块 c 中,这样就完成了 Vuex 的模块收集操作;
测试模块树的构建结果:

根模块 root 对象中,包含两个子模块:模块 A 和模块 B;
其中,模块 A 包含一个子模块:模块 C;
构建结果与期望相符,模块树构建完成;
五,结尾
本篇,主要介绍了 vuex 模块收集是如何实现的,主要包括以下几点:
Vuex 模块的概念;
Vuex 模块的使用和命名空间;
Vuex 模块收集的实现;
下一篇,继续介绍 Vuex 模块安装的实现;
维护日志
20210921:
全文重新调整,细化标题、完善文章摘要;
20210922:
补充 “Vuex 模块收集的实现” 部分,之前因牙龈发炎欠债还清;