提示语
解读源码时,总会使用到 util 工具库中的方法。为了文章的可读性,所以用到那个就解读那个。
util 工具库
forEachValue方法,遍历对象中的属性。
export function forEachValue (obj, fn) {
// 用 Object.keys 将 obj 对象的自身可枚举属性组成数组并还回,
// 然后,再用数组方法 forEach 遍历此数组每一项
Object.keys(obj).forEach(key => fn(obj[key], key))
}
复制代码
assert方法,抛出错误提示
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
复制代码
注册模块
对vuex源码已有所了解的小伙伴都知道,Store类在初始化内部状态时,调用了一个类——ModuleCollection。
export class Store {
// options = { state, getters, actions, mutations, modules }
constructor(options = {}) {
// ...
this._modules = new ModuleCollection(options)
// ...
}
// ...
}
复制代码
这个类会通过其内部定义的register方法,对传入的modules模块对象进行注册——从根模块到其子模块。
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
/*
* path:按序存储根模块到其子模块的变量名(注:根模块没有变量名,所以path初始为空数组)。
* rawModule: 源模块数据。
* runtime:布尔值,控制模块的注销。
*/
register (path, rawModule, runtime = true) {
// 生产环境下 __DEV__ 为 true
if (__DEV__) {
// 判断传入的 getters、actions、mutations 是否符合
// 其类型规范('function' 或 'object'),不符合就抛出错误
assertRawModule(path, rawModule)
}
// 创建新的模块对象
const newModule = new Module(rawModule, runtime)
if (path.length === 0) { // 根模块
// 存储根模块
this.root = newModule
} else { // 子模块
// 获取当前子模块( newModule)的父模块
const parent = this.get(path.slice(0, -1))
// 将当前子模块(newModule)插入其父模块的 _children 对象下
parent.addChild(path[path.length - 1], newModule)
}
// 递归,注册嵌套的模块
if (rawModule.modules) {
// 遍历 modules 模块
forEachValue(rawModule.modules, (rawChildModule, key) => {
// path.concat(key) 将当前模块及其子模块按序存到 path 数组中
// 假设:b模块 嵌套了 a模块,那么 path 数组形式:['b', 'a']
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// ...
}
复制代码
我们可以看到,在注册模块的过程中,会调用类——Module。这个类可以创建一个新的模块对象,且定义了新对象的基本数据结构和一些内部方法。
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 存储子模块
this._children = Object.create(null)
// 存储源模块
this._rawModule = rawModule
// 存储源模块的 state
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// 判断模块是否启用——命名空间
get namespaced () {
return !!this._rawModule.namespaced
}
// 添加子模块
addChild (key, module) {
this._children[key] = module
}
// 删除子模块
removeChild (key) {
delete this._children[key]
}
// 获取子模块
getChild (key) {
return this._children[key]
}
// 判断子模块是否已存在
hasChild (key) {
return key in this._children
}
// 更新模块
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
// 遍历子模块
forEachChild (fn) {
forEachValue(this._children, fn)
}
// 遍历 getters
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
// 遍历 actions
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
// 遍历 mutations
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
复制代码
创建新的模块对象后,会根据path.length来处理根模块和子模块。
if (path.length === 0) { // 根模块
// 存储根模块
this.root = newModule
} else { // 子模块
// 获取当前子模块( newModule)的父模块
const parent = this.get(path.slice(0, -1))
// 将当前子模块(newModule)插入其父模块的 _children 对象下
parent.addChild(path[path.length - 1], newModule)
}
复制代码
对于根模块的处理,很容易明白。但,对于子模块的处理,理解起来,可能要稍费一点功夫。我们可以分三步来看:
-
path.slice(0, -1)
表示抽取 path 数组第一位元素到其最后一个元素(但不包含最后一个元素,也就是只会取到倒数第二个元素)。 -
调用
this.get
获取模块。这是ModuleCollection类内部定义的,专门用来获取模块的方法。理解此方法的关键有两点:其一,了解数组方法reduce。其二,path数组按序存储根模块到其子模块的变量名(最好通过调试打印出来看看,这样会更容易理解)。
get (path) {
return path.reduce((module, key) => {
return module.getChild(key) // 获取当前父模块的子模块
}, this.root)
}
复制代码
- 调用模块的addChild方法,将子模块( newModule)插入其父模块的 _children 对象下。
parent.addChild(path[path.length - 1], newModule)
复制代码
对创建的模块 newModule 进行处理之后,会判断其是否有modules对象。若是有,则通过forEachValue方法,循环注册其所有子模块。这种通过递归注册的方式,会持续到注册的模块下没有modules对象时,才会终止。
// 递归,注册嵌套的模块
if (rawModule.modules) {
// 遍历modules模块
forEachValue(rawModule.modules, (rawChildModule, key) => {
// path.concat(key) 将当前模块及其子模块按序存到 path 数组中
// 假设:b模块 中嵌套了 a模块,那么 path 形式为:['b', 'a']
this.register(path.concat(key), rawChildModule, runtime)
})
}
复制代码
其它方法
ModuleCollection 类中的其它方法。
- unregister 注销父模块中的子模块。
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
const child = parent.getChild(key)
if (!child) {
if (__DEV__) {
console.warn(
`[vuex] trying to unregister module '${key}', which is ` +
`not registered`
)
}
return
}
// runtime:布尔值,默认为true。在注册模块时设置。
if (!child.runtime) {
return
}
parent.removeChild(key)
}
复制代码
- isRegistered 判断子模块在父模块中是否已存在。
isRegistered (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (parent) {
return parent.hasChild(key)
}
return false
}
复制代码
- getNamespace 用 ‘/’ 将开启命名空间的模块拼接起来。
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
复制代码
若是对这个方法感到困惑不理解,那么就要在读一下vuex模块命名空间,只有了解其使用方式,才能更加容易理解它的源码。下图是对此方法调试的打印。
vuex源码中提供了用于调试的案例,我选的是counter,且在其中定义了三个模块,仅moduleC模块未开启命名空间。文件路径:examples/counter/store.js
在Store类的installModule方法中打印。文件路径:src/store.js
控制台打印结果,可以看到,只拼接了开启命名空间的模块。
- update 更新模块的接口
update (rawRootModule) {
// 下面的update是 module-collection.js 文件中定义的辅助方法,是真正实现
// 更新模块的方法。
update([], this.root, rawRootModule)
}
复制代码
辅助型方法
- update 真正实现更新模块的方法
function update (path, targetModule, newModule) {
if (__DEV__) {
// 判断模块中的 getters、actions、mutations 是否符合
// 其类型规范('function' 或 'object'),不符合就抛出错误
assertRawModule(path, newModule)
}
// 更新目标模块
targetModule.update(newModule)
// 更新模块嵌套
if (newModule.modules) {
for (const key in newModule.modules) {
if (!targetModule.getChild(key)) {
if (__DEV__) {
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]
)
}
}
}
复制代码
- assertRawModule 判断模块中的 getters、actions、mutations 是否符合其类型规范
// assert:检测类型
// expected:类型提示
const functionAssert = {
assert: value => typeof value === 'function',
expected: 'function'
}
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
}
function assertRawModule (path, rawModule) {
Object.keys(assertTypes).forEach(key => {
if (!rawModule[key]) return // 模块中是否定义getters、actions或 mutations
const assertOptions = assertTypes[key]
forEachValue(rawModule[key], (value, type) => {
// util 中定义的断言函数,用来抛出错误提示。
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
}
复制代码
最后,我们来看一下调用new ModuleCollection(options)
最终得到的对象是啥样的。
结束语
希望我的浅显解读,能够帮到热爱学习的同学们。同时,也诚请同学们能顺手给个大大的赞,我会继续努力掉头发的。