Vuex@next源码解析 - module-collection篇
前言
Vuex4.0源码解析的第二篇
这篇主要讲module-collection.js这个文件,主要是ModuleCollection这个类
前情提要
在第一篇帖子中,我们知道,在new一个Store时
构造函数constructor会执行this._modules = new ModuleCollection(options)
这个操作会挂载根模块的state到this._modules.root,然后后续在installModule中递归的合并各个模块的状态
在installModule,并不会对根模块的state进行安装
而是安装根模块的getters,mutations,actions递归安装子模块(这里就会合并状态了,如下所示)
1 | // installModule的里的判断 |
前面我们说过,installModule的主要功能就是每个模块合并state成为一个单独的对象
合并所有的getter到_wrappedGetters
可以看到if内的逻辑是先获取父模块的state,然后删除子模块的state
如果安装根模块的state话,那么是无法找到它的父模块的state,无法找到也就无法删除
而且Store类的中registerModule,unregisterModule和hasModule可以说是直接依赖本模块实现。
module-collection.js
这个类创建的对象挂载到了store._modules

1 | export default class ModuleCollection { |
可以看到这个类相比Store简洁不少
constructor
1 | export default class ModuleCollection { |
在构造函数中,直接使用register方法来注册根模块的state
这里的注释也表明了传入的参数rawRootModule就是传入createStore函数的参数
在Store的constructor中的this._modules = new ModuleCollection(options)就执行了这段逻辑
register
这个方法可以说是这个类最重要的方法也不为过,因为个人感觉很多情况下并不会卸载模块(可能我场景见得少😂)
1 | export default class ModuleCollection { |
开头执行了assertRawModule,来判断参数是否合法(这个后面有写,可以通过右边的'内置函数'跳转查看)
接下来new了一个Module类(这个类下一篇会写)的对象,Module对象的创建完全由Module类来负责(因为把rawModule参传了进去)
1 | const newModule = new Module(rawModule, runtime) |
从判断可以看出,如果path长度为0,证明当前挂载的是根模块,直接挂载到root属性上
如果path长度不为0,说明当前模块是某个模块的子模块
那么通过get方法获取了对应父模块(slice(0, -1))的Module对象
最后调用Module对象的addChild方法对子模块进行添加
最后会递归的处理子模块
1 | // register nested modules |
注意这里也使用了path路径数组的方式,如果现在是在注册根下的m1模块
那么此时path.concat(key)为['m1']
这里注意这个方法是支持挂载静态或者动态模块的,他的第三个参数是可以传入true或者false的(默认为true)
但是对于Store暴露的registerModule,它里面的逻辑是不传第三个参数
1 | export class Store { |
所以导致了我们只能注册一个动态的模块
而在这个类的构造器中的注册模块是传入第三个参数为false的
1 | export default class ModuleCollection { |
unregister
和register的作用相反,用于卸载一个模块对象
1 | export default class ModuleCollection { |
先通过get方法获取了对应父模块(slice(0, -1))的Module对象
1 | const parent = this.get(path.slice(0, -1)) |
然后取path的最后一个元素,对应着要删除模块的名字
1 | const key = path[path.length - 1] |
调用Module对象的getChild方法来获取这个要删除的子模块
1 | const child = parent.getChild(key) |
接着对模块进行是否存在的判断,以及是否为运行时注入模块child.runtime
只有动态模块(runtime为true)才能被卸载(也就是删除)
最后调用Module对象的removeChild来删除一个这个模块
isRegistered
1 | export default class ModuleCollection { |
这个函数非常简单,找到父模块对象,通过Module对象的hasChild判断是否存在
update
1 | export default class ModuleCollection { |
可以看到这个函数的实现依赖了内部函数update,在Store类中的hotUpdate调用了这个方法
可以看出这个update是用来更新根模块的,因为path参数传入了一个空数组,targetModule传入了this.root,也就是根模块对象
详情可以看左侧内部函数update部分
get
1 | export default class ModuleCollection { |
这个函数可以看出,想要取得某个Module对象
都是通过根模块this.root开始,根据path参数来进行一层一层的通过reduce迭代的,每次的迭代都会通过Module的getChild来获取他的子模块
getNamespace
简单点讲,这和函数就是根据path来生成命名空间
1 | export default class ModuleCollection { |
注意这里会判断每个module的namespaced,也就是是否开启命名空间
来确定如何添加命名空间,对于namespaced为false的,一律不添加,(也就是三元判断:之后的部分)
这会有一种很有趣的现象,如下
1 | const store = createStore({ |
上面这段代码会报错,因为存在了相同的命名空间m1,按照人的思维,m2里面的m1和根里面的m1不是同一个模块才对

原因就是Vuex只会对配置namespaced为true的模块添加其名字到命名空间路径字符串中
而不是说某个模块namespaced为true,那么生成的命名空间路径字符串是它当前的具体位置,比如例题m2模块下的m1命名空间路径字符串不为m2/m1/
内部函数
assertRawModule
这个函数主要是对各个模块内的输入类型进行判定
1 | function assertRawModule (path, rawModule) { |
Vuex中会判断getters,mutation,actions的输入合法性,这里使用的是一种策略模式
把验证策略定义在了assertTypes变量中
1 | const assertTypes = { |
这里有两种策略functionAssert和objectAssert
其中getters和mutations,都对应了functionAssert,可以从策略的assert验证函数看出该策略判断是否为函数
而actionAssert对应objectAssert,该策略允许函数或者带handler属性(且handler为一个函数)的对象
assertRawModule函数通过Object.keys对assertTypes遍历来对每一种类型进行验证
1 | Object.keys(assertTypes).forEach(key => { |
如果模块没传入值,那么不用判断
1 | if (!rawModule[key]) return |
如果传入值了,那么获取对应类型的策略对象
1 | const assertOptions = assertTypes[key] |
然后通过一个工具forEachValue函数遍历,用assert(这个函数工具篇讲过)进行判断,
1 | forEachValue(rawModule[key], (value, type) => { |
报错的内容根据makeAssertionMessage这个函数进行生成
1 | function makeAssertionMessage (path, key, type, value, expected) { |
这个函数的key就是策略的类型名字字符串,就是getters,mutations,actions中的一个
type就是这种类型对应一个属性的名字,value就对应这个属性的值
比如现在给getters配置一个对象,如下
1 | const store = createStore({ |
那么此时不符合getters策略的验证函数,通过makeAssertionMessage生成了
1 | getters should be function but getters.g1 in module is {}` |
我们可以试验一下
1 | const store = createStore({ |
发现控制台确实报了错误

update
在这个函数中,对targetModule(Module对象)进行递归的更新
新的模块为newModule,就类似我们传入createStore的对象。
1 | function update (path, targetModule, newModule) { |
开头依然是通过assertRawModule来判断参数是否合法
1 | if (__DEV__) { |
然后调用Module对象的update来更新模块
1 | // update target module |
接着就是如果存在modules属性,也就是存在子模块
那么遍历这些模块,然后递归的调用update来更新接下来的模块
这里注意如果父模块不存在子模块,那么更新会失败,因为此时不应该叫更新了,而应该是注册一个新的模块
不过这里有一点奇怪的地方,对于不存在的子模块,跳过更新遍历下一个可能更符合逻辑?
直接return,如果下个模块存在,也更新不到了,感觉这不符合更新的逻辑欸…
1 | if (newModule.modules) { |
后记
这是Vuex的第二篇了,这篇的篇幅就比Store篇短了许多
今天翻了翻Vuex的Issue,发现已经有Vuex5的相关提案了,感觉Vuex4会是个过渡的版本
不过我觉得应该没那么快出到Vuex5,可能会加入新的功能到Vuex4中,Vuex5应该还比较远
看了下API的提案,发现越来越有函数式的味道了,以下为提案的创建Store例子
1 | const category1 = createStore(() => { |
只能说人还是太能折腾了哈哈哈哈哈哈,不过折腾的最终目标都是为了产出更加优秀的代码
这点我觉得是非常重要的,希望Vue越来越好~~~