怎么实现vuex源码的核心类ModuleCollection
本文想写 vuex 源码中核心的一个类,ModuleCollection
,这个类是将参数格式化成一定的结构。
感觉这种模式,在别的源码里也常用,希望自己能真正理解并运用,所以特地有此篇。
vuex 使用的时候,传入类似这种参数
export default new Vuex.Store({ modules: { aModule: { state: { msg: "a模块的颜酱" }, }, bModule: { state: { msg: "b模块的颜酱" }, modules: { cModule: { state: { msg: "c模块的颜酱" }, } } } }, state: { msg: "hi, i am 颜酱" } });
使用 c 模块的state
的时候,是this.$store.state.b.c.msg
。
vuex
为了方便使用参数,对参数,处理成如下格式,其实就是将父子关系更加明显化。
{ "_raw": 根模块数据, "state": { "msg": "hi, i am 颜酱", }, "_children": { "aModule": { "_raw":a模块的原来数据, "state": { "msg": "a模块的颜酱" }, "_children": {}, }, "bModule": { "_raw": b模块的原来数据, "state": { "msg": "b模块的颜酱" }, "_children": { "cModule": { "_raw": c模块的原来数据, "state": { "msg": "c模块的颜酱" }, "_children": {}, } } } } }
每个模块都有的字段:_raw、state、children
我的思路:简单封装函数,返回正确的格式
开始,我写了个formatData
方法,但不够优雅哇。
其实这个先试着自己写,处理完第一层逻辑,基本就ok,核心就是递归,只是递归的时候,注意children
这个对象,这也算是这里的难点。
const options = { modules: { aModule: { state: { msg: "a模块的颜酱" } }, bModule: { state: { msg: "b模块的颜酱" }, modules: { cModule: { state: { msg: "c模块的颜酱" } } } } }, state: { msg: "hi, i am 颜酱" } }; const formatData = (options, res) => { res._raw = options; res.state = options.state; const childModules = options.modules; // 如果有模块的话 if (childModules) { // 遍历模块 Object.keys(childModules).forEach(childModuleName => { const curChildModule = childModules[childModuleName]; // 这边没有的必须先设置{},这样才能赋值属性 res.children = res.children ? res.children : {}; res.children[childModuleName] = res.children[childModuleName] ? res.children[childModuleName] : {}; // 以上两行代码都是为第二个参数准备的,防止undefined报错 formatData(curChildModule, res.children[childModuleName]); }); } }; // 因为res是另外的,所以封装个高阶函数,这也使用起来很方便 const format = options => { let res = {}; formatData(options, res); return res; }; console.log(format(options));
vuex源码的思路:封装类,以路径的方式注册每个模块
源码很赞的是,以类的形式出现,核心逻辑是传入模块的路径和模块,来注册在相应的位置。
比如:根模块有b模块,b模块里有c模块,c模块里有d模块,那么d模块的路径就是[b,c,d]
注册d模块时候,其实这样就可以:
root.children.b.children.c.children.d = dModule
当然实际情况是,路径是动态的,需要我们写个函数去实现这样的功能:
为方便实现,将上面代码拆分下
// d的父模块 const parentModule = root.children.b.children.c // 父模块里注册当前模块 parentModule.children.d = dModule
这样拆分的好处是,parentModule
可以通过函数很方便的拿到
function register(path,module){ const init = root const fn = (acc,cur) => acc[children][cur] // 父模块,这里想象成上面实际的例子 const parentModule = path.slice(0,-1).reduce(fn,init) // 父模块里注册当前模块 const curModuleName = path[path.length-1] parentModule.children[curModuleName] = module }
以上就是最核心的逻辑了,当然这里的root
还需要赋值的,其他的两个属性也需要加上,但这都不是事,不另做解释
const options = { modules: { aModule: { state: { msg: "a模块的颜酱" } }, bModule: { state: { msg: "b模块的颜酱" }, modules: { cModule: { state: { msg: "c模块的颜酱" } } } } }, state: { msg: "hi, i am 颜酱" } }; class ModuleCollection { constructor(options) { // 这里的root存放格式化之后的数据 this.root = null; // 首次注册的是根模块,根模块的路径是空的 this.register([], options); } register(path, module) { let moduleItem = { _raw: module, // 注意这里的children是对象 children: {}, state: module.state }; // 第一次肯定是根模块,根模块必须赋值 if (!this.root) { this.root = moduleItem; } else { // 核心代码:根据路径注册模块 const curModuleName = path[path.length - 1]; const fn = (acc, cur) => acc.children[cur]; const parent = path.slice(0, -1).reduce(fn, this.root); parent.children[curModuleName] = moduleItem; } // 当前模块有子模块的话,继续挨个注册 const childModules = module.modules; childModules && Object.keys(childModules).forEach(childModuleName => { const curChildModule = childModules[childModuleName]; // concat用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。 this.register(path.concat(childModuleName), curChildModule); }); } } const modules = new ModuleCollection(options); // 这就是格式化之后的数据了 console.log(modules.root); console.log(modules.root.children.bModule.children);