ModuleCollection与Module类
上一篇文章《源码基本结构与install函数》中我们已经详细讲解了Vuex
的整个源码的基本结构以及install
函数的实现。按照读源码的节奏我们接下来应该分析Store
类的实现。但是,我在编写关于《Store实例化》这篇文章时,发现由于Store
类的初始化依赖ModuleCollection
和Module
类,为了让文章的表达更加清晰,我需要假设由这两个类创建的实例的存在,并用大量的注释来解释ModuleCollection
和Module
实例的结构,进而使源码的分析得以进行。正因如此,会使《Store实例化》这篇文章显得十分冗杂。因此,在编写文章时,我先把ModuleCollection
和Module
类的实现提到前面来讲解,目的是为了后面讲解Store
类的实现时能够更加清晰顺利。
当然,你在实际中阅读源码时应该先阅读Store
实例化的实现,再阅读ModuleCollection
和Module
类的实现。阅读时,你可以通过断点调试的方式来查看ModuleCollection
和Module
类的实例结构即可。
# 一、简述Store类的构造函数
在Store
类构造函数中有如下代码:
constructor(options = {}) {
// ...
// 初始化各种实例属性
this._modules = new ModuleCollection(options) // 创建根模块树
// ...
installModule(this, state, [], this._modules.root) // 安装模块
resetStoreVM(this, state) // 响应式处理
// 安装插件
// ...
}
2
3
4
5
6
7
8
9
10
11
12
Store
实例化时,会经过以下几个步骤:
- 初始化各种实例属性
- 创建根模块树并赋值给实例属性
_modules
- 根据根模块树,安装模块。安装时会递归安装子模块
- 响应式处理
- 安装插件
在实例化的构成中,安装模块和响应式处理是Store
类的核心,这两个操作都依赖ModuleCollection
类创建的_modules
实例属性。而Module
类本质上是用来创建一个封装了增删改查方法的实例对象,它被ModuleCollection
类用来创建模块树。我们后文先从Module
类开始讲解,再讲解ModuleCollection
类。
# 二、Module类
Module
类是Vuex
中用来创建模块实例的类,其定义在文件module/module.js
。Module
类代码:
export default class Module {
constructor(rawModule, runtime) {
}
get namespaced() {
}
// ...
}
2
3
4
5
6
7
8
9
10
# 1. 构造函数
constructor(rawModule, runtime) {
this.runtime = runtime // 是否运行时
this._children = Object.create(null) // 存放子模块
this._rawModule = rawModule // 记录模块原始的配置对象
const rawState = rawModule.state // 模块原始的state
// 初始化state, state是函数时则执行函数并返回结果,否则直接返回state,默认值是{}
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
2
3
4
5
6
7
8
Module
类的构造函数是比较简单的,它接收两个参数:
rawModule
:原始模块对象,在实例化时传入的是模块的配置对象,其中包含了state
、mutations
、actions
、getters
、moudles
等属性。runtime
:是否运行时,用来标记模块是静态注册的还是动态注册的,后文详解
构造主要完成了以下几件事情:
- 增加
runtime
,用来标记模块是静态注册的还是动态注册的 - 创建
_children
属性,用来存放子模块 - 创建
_rawModule
属性,该属性记录了传入的原始的配置对象 - 增加
state
属性,该属性从rawModule.state
获取,如果state
属性是函数,则执行函数并返回结果,否则直接返回state
属性,默认值是{}
。
# 2. 访问属性namespaced
get namespaced() {
return !!this._rawModule.namespaced
}
2
3
当访问namespaced
属性时,会返回_rawModule.namespaced
转为布尔值后的结果。
# 3. 模块的增删查改
// 添加子模块
addChild(key, module) {
this._children[key] = module
}
// 删除指定子模块
removeChild(key) {
delete this._children[key]
}
// 获取指定子模块
getChild(key) {
return this._children[key]
}
// 更新模块,只会更新模块的actions、mutations、getters、namespaced
update(rawModule) {
this._rawModule.namespaced = rawModule.namespaced // 更新namespaced标记
if (rawModule.actions) { // 更新actions
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) { // 更新mutations
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) { // 更新getters
this._rawModule.getters = rawModule.getters
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
其中增删查都是根据给定的key
值,对实例的_children
属性进行操作。而更新操作是更新实例的_rawModule
属性。在更新操作中,我们发现,更新操作只会更新actions
、mutations
、getters
、namespaced
四个属性,而state
并没有包含在内,为什么呢?
# 4. 各种遍历方法
在Module
类中,还有如下几个遍历相关属性后执行回调的方法:
// 遍历子模块并执行指定的操作, fn会接收子模块及其子模块的key作为参数
forEachChild(fn) {
forEachValue(this._children, fn)
}
// 遍历getters并执行指定的操作, fn会接收getters及其key作为参数
forEachGetter(fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
// 遍历actions并执行指定的操作, fn会接收actions及其key作为参数
forEachAction(fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
// 遍历mutations并执行指定的操作, fn会接收mutations及其key作为参数
forEachMutation(fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
forEachValue
函数是一个遍历对象并执行指定回调的函数,并将对象的key和value作为参数传递给回调函数,定义如下:
export function forEachValue(obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
2
3
可以看到,这些方法分别都接收一个回调函数,分别遍历_children
、_rawModule.getters
、_rawModule.actions
、_rawModule.mutations
这四个属性,并执行指定的回调。
# 5. Module类总结
以上就是Module
类完整的代码,可以看到是比较简单的。其主要功能就是根据传入的配置对象,创建一个封装了增删查改方法的实例对象。一个Module
实例最终结构如下:
{
runtime: false, // 用于模块是标记静态注册还是动态注册的
state: { // 原始的options.state的获取结果
count: 0
},
_children: {}, // 子模块
_rawModule: {}, // 传入的原始options配置对象
get namespaced: () => {}, // 是否开启了命名空间
addChild: (key, module) => {}, // 添加子模块
removeChild: (key) => {}, // 删除子模块
getChild: (key) => {}, // 获取子模块
update: (rawModule) => {}, // 更新模块
forEachChild: (fn) => {}, // 遍历子模块_children并执行指定的操作
forEachGetter: (fn) => {}, // 遍历_rawModule.getters并执行指定的操作
forEachAction: (fn) => {}, // 遍历_rawModule.actions并执行指定的操作
forEachMutation: (fn) => {} // 遍历_rawModule.mutations并执行指定的操作
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Module
类的基本结构如下图:
# 三、ModuleCollection类
ModuleCollection
类是Vuex
中用来根模块树的类,其定义在文件module/module-collection.js
:
export default class ModuleCollection {
constructor(options) {
// ...
}
get(path) {
// ...
}
// ...
}
2
3
4
5
6
7
8
9
# 1. 构造函数
constructor(rawRootModule) {
// register root module (Vuex.Store options)
// 注册根模块,并标记为静态注册
this.register([], rawRootModule, false)
}
2
3
4
5
构造函数接收一个参数,该参数是根模块的配置对象,在调用new Store(options)
时会执行以下代码:
constructor(options = {}) {
// ...
this._modules = new ModuleCollection(options) // 创建根模块树
// ...
}
2
3
4
5
即rawRootModule
为Vuex.Store
传入的options
对象。接收参数后,ModuleCollection
的构造函数只调用了实例方法register
,下面来看看register
方法的定义。
# 2. 模块注册register
register(path, rawModule, runtime = true) {
// 断言传入的vuex配置对象是否合法,对参数类型做校验
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
// 创建模块实例,Module实例是一个封装了对rawModule操作的实例,
// 包括子模块的增删查改,以及模块actions,getter,mutations等操作
const newModule = new Module(rawModule, runtime)
if (path.length === 0) { // 路径为空则注册根模块
this.root = newModule
} else { // 否则注册子模块
// 获取需要注册的子模块的父模块
const parent = this.get(path.slice(0, -1))
// 添加子模块
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 遍历子模块,递归注册子模块
if (rawModule.modules) { // 模块中有modules属性则遍历
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
register
方法接收三个参数:path
:当前模块的路径,类型为数组,当注册根模块时,传入的是空数组rawModule
:当前要注册的模块配置对象,当注册根模块时,传入的即为new Vuex.Store(options)
中的options
对象runtime
:用于标记是否是运行时注册(即动态注册),当注册根模块时,其传入的值是false
,代表静态注册,区分的原因见下文
register
方法主要做了以下几件事情:- 断言传入的vuex配置对象是否合法,即对参数类型做校验(见下文)
- 创建一个
Module
实例,该实例封装了对rawModule
的操作 - 如果当前注册的是根模块,则将
newModule
赋值给this.root
,否则根据指定的路径,逐级从this.root
找到相应父模块,然后调用addChild
方法为其添加子模块 - 如果
rawModule
中有modules
属性,说明其具有子模块,则遍历modules
,递归调用register
方法注册子模块
以下代码为例:
const store = new Vuex.Store({
state: {
a: 1,
b: 2
},
getters: {
total: state => state.a + state.b
},
modules: {
user: {
namespaced: true,
state: {
lastName: 'Foo',
firstName: 'Bar'
},
getters: {
fullName: state => state.lastName + ' ' + state.firstName
}
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
经过以上处理后,在Store
实例中,其this._modules
属性会是类似下面的结构:
this._modules = {
root: {
runtime: false,
state: {}, // 原始的options.state
_rawModule: {} // 原始的options配置对象
get namespaced: false,
_children: {
user: {
runtime: false,
state: {}, // 原始的options.modules.user.state
_rawModule: {} // 原始的options.modules.user配置对象
get namespaced: true,
_children: {}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上文我们提到,runtime
参数是用于标记区分动态注册和静态注册的,那么为什么要这样的区分呢?Vuex之所以提供动态注册的能力是为了提高Vuex的灵活性,比如一些第三方的插件可以通过动态注册在store
实例中附加新的模块来增强或者使用Vuex的状态管理,如vuex-router-sync
插件 (opens new window))。动态注册的模块可以被卸载,然而Vuex初始化时的模块不应该在运行时被卸载,这是确保Vuex状态管理的可维护性和一致性考虑。总的来说在保证Vuex的灵活性的同时,有以下的目的:
- 保证一致性,简化状态管理的管理过程,使代码更加清晰,易于维护
- 保证可读性和可预测性:使得状态树的结构在实例化时就被确定下来
- 易于跟踪状态的变化,使其更好的与Vue开发者工具集成
# 3. 模块卸载unregister
如上文所说,Vuex是可以对动态注册的模块进行卸载的,unregister
就是用来卸载动态模块的方法
// 卸载一个模块
unregister(path) {
const parent = this.get(path.slice(0, -1)) // 获取父模块
const key = path[path.length - 1] // 获取子模块的key
if (!parent.getChild(key).runtime) return // 静态注册的不可卸载
parent.removeChild(key) // 删除子模块
}
2
3
4
5
6
7
8
unregister
方法接收一个参数,即要卸载的模块的路径,该路径为数组,如['user']
,根据路径找到相应的父模块,如果获取到的模块是个动态注册的模块则调用父模块的removeChild
方法将其从父模块中移除。
# 4. 更新模块
update(rawRootModule) {
update([], this.root, rawRootModule)
}
2
3
update
方法用于更新整个模块树,接收一个参数,即要更新的模块配置对象。在update
方法中会调用外部的update
函数,update
函数定义如下:
function update(path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}
// update target module 更新模块
targetModule.update(newModule)
// update nested modules
// 递归更新子模块
if (newModule.modules) {
for (const key in newModule.modules) {
if (!targetModule.getChild(key)) { // 原模块不存在当前的模块则不注册
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
与register
方法类似,更新方法会先对新的模块进行校验,然后调用targetModule.update
方法更新模块,如果新模块中有子模块且在原来的模块中有相应的定义,则递归调用update
方法更新子模块。其中targetModule.update
即是调用Module
实例的update
方法,有上文Module
类的分析知道该方法会覆盖掉targetModule
的getters
、mutations
、actions
、namespaced
等属性,而不会覆盖掉targetModule
的state
属性。
# 5. 其它实例方法
除了以上方法以外,ModuleCollection
类还有get
和getNamespace
两个实例方法。
get
方法定义如下:
get(path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
2
3
4
5
其功能是接收一个数组类新的path
路径,然后从this.root
逐级往下查找模块,最终返回相应的模块。如
getNamespace
方法定义如下:
getNamespace(path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
2
3
4
5
6
7
其功能是接收一个数组类新的path
路径,然后从this.root
逐级往下查找模块,如果查找的模块中有namespaced
属性,则拼接上/
,最后返回一个以/
分隔的命名空间字符串。
如下例子:
this._modules = {
root: {
runtime: false,
state: {}, // 原始的options.state
_rawModule: {} // 原始的options配置对象
get namespaced: false,
_children: {
user: {
runtime: false,
state: {}, // 原始的options.modules.user.state
_rawModule: {} // 原始的options.modules.user配置对象
get namespaced: true,
_children: {}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
调用get(['user'])
会返回root._children.user,调用getNamespace(['user'])
会返回user/
。
# 6. 参数校验
在更新和注册模块的时候都会执行如下代码:
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
2
3
assetRawModule
方法对模块参数的合法性进行校验,相关函数定义如下:
// 函数断言配置
const functionAssert = {
assert: value => typeof value === 'function',
expected: 'function'
}
// 对象断言配置
// 函数或者含有类型为函数的handler属性的对象
const objectAssert = {
assert: value => typeof value === 'function' ||
(typeof value === 'object' && typeof value.handler === 'function'),
expected: 'function or object with "handler" function'
}
// 断言的类型配置
const assertTypes = {
getters: functionAssert,
mutations: functionAssert,
actions: objectAssert
}
// 断言rawModule是否合法
function assertRawModule(path, rawModule) {
// 遍历getters,mutations,actions
Object.keys(assertTypes).forEach(key => {
if (!rawModule[key]) return
// 获取断言配置
const assertOptions = assertTypes[key]
// 遍历getters,mutations,actions
forEachValue(rawModule[key], (value, type) => {
// 执行断言,不通过时抛出错误
assert(
assertOptions.assert(value),
makeAssertionMessage(path, key, type, value, assertOptions.expected)
)
})
})
}
// 根据传入的配置生成断言错误信息
function makeAssertionMessage(path, key, type, value, expected) {
let buf = `${key} should be ${expected} but "${key}.${type}"`
if (path.length > 0) {
buf += ` in module "${path.join('.')}"`
}
buf += ` is ${JSON.stringify(value)}.`
return buf
}
// utils.js
// 断言失败抛出异常
export function assert(condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
其功能就是遍历相关参数,对getters
、mutations
、actions
配置做类型校验,如果校验失败就抛出一个错误。基本的处理过程是:遍历模块的getters
、mutations
、actions
属性,如果已经定义了,则再遍历其键值对,通过functionAssert
、objectAssert
定义的断言类型对配置的值进行断言,如果校验失败就抛出一个错误。
# 7. ModuleCollection总结
ModuleCollection
类主要用来管理模块的注册、卸载和更新,其核心方法是register
、unregister
、update
,其中register
和unregister
方法主要用来注册和卸载模块,update
方法主要用来更新模块。ModuleCollection
类还有get
和getNamespace
两个实例方法,get
方法用于根据给定的路径获取模块,getNamespace
方法用于更具给定的路径获取命名空间。
在new Store(options)
中初始化_modules
属性时,最终会得到一个包含root
属性的ModuleCollection
实例,options
中所有的信息都会被记录到root
属性中,用于后续安装根模块。
ModuleCollection
类的基本结构如下图: