Bootstrap

【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

启动服务,测试模块状态取值:

发现问题

测试状态更新:点击同步更新按钮

根模块和 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 命名空间后,再点击更新按钮,模块中的状态不会发生改变:

这时,触发对应模块的数据更新需要添加模块的命名空间标识

测试效果

四,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 模块收集的实现” 部分,之前因牙龈发炎欠债还清;