vuex 源码分析(2)—— ModuleCollection 注册模块

提示语

解读源码时,总会使用到 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)
}

对于根模块的处理,很容易明白。但,对于子模块的处理,理解起来,可能要稍费一点功夫。我们可以分三步来看:

  1. path.slice(0, -1) 表示抽取 path 数组第一位元素到其最后一个元素(但不包含最后一个元素,也就是只会取到倒数第二个元素)。

  2. 调用this.get获取模块。这是ModuleCollection类内部定义的,专门用来获取模块的方法。理解此方法的关键有两点:其一,了解数组方法reduce。其二,path数组按序存储根模块到其子模块的变量名(最好通过调试打印出来看看,这样会更容易理解)。

get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key) // 获取当前父模块的子模块
    }, this.root)
  }
  1. 调用模块的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 类中的其它方法。

  1. 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)
  }
  1. 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
}
  1. 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

c1.jpg

在Store类的installModule方法中打印。文件路径:src/store.js

c2.jpg

控制台打印结果,可以看到,只拼接了开启命名空间的模块。

c3.jpg

  1. update 更新模块的接口
  update (rawRootModule) {
    // 下面的update是 module-collection.js 文件中定义的辅助方法,是真正实现
    // 更新模块的方法。
    update([], this.root, rawRootModule)
  }

辅助型方法

  1. 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]
      )
    }
  }
}
  1. 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)
      )
    })
  })
}
  1. 错误信息
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)最终得到的对象是啥样的。

c4.jpg

结束语

希望我的浅显解读,能够帮到热爱学习的同学们。同时,也诚请同学们能顺手给个大大的赞,我会继续努力掉头发的。

src=http___image.biaobaiju.com_uploads_20190318_16_1552896543-WgOkfpNFeu.jpg&refer=http___image.biaobaiju.jpg