从未看过源码,到底该如何入手?分享一次完整的源码阅读过程(二)

简介: 接上文。

3.1.1 递归收集模块


Module-collection.js 文件中定义了 ModuleCollection 类,其作用就是通过递归遍历 options 入参,将每个模块都生成一个独立的 Moudle


这里先来熟悉一下 options 的结构,如下:


import Vuex from 'vuex'
const options = {
  state: {...},
  getters: {...},
  mutations: {...},
  actions: {...},
  modules: {
    ModuleA: {
      state: {...},
      ...
      modules: {
        ModuleA1: {...}
      }
    },
    ModuleB: {
      state: {...},
      ...
      modules: {
        ModuleB1: {...}
      }
    }
  }
}
const store = new Vuex.Store(options)
export default store


可以看到传入的 options 整体可以看成一个根模块 root ,然后 rootmodules 中嵌套着另外两个子模块:ModuleAModuleB ,而 ModuleAModuleB 内部也分别嵌套着一个子模块,分别为 ModuleA1ModuleB1


这样就组成了一个模块树,因此 ModuleCollection 类的工作就是将保留原来的模块关系,将每个模块封装到一个 Module 类中


export default class ModuleCollection {
  constructor (rawRootModule) {
    // 递归注册模块
    this.register([], rawRootModule, false)
  }
  // 根据路径顺序,从根模块开始递归获取到我们准备添加新的模块的父模块
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  // 递归注册模块
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }
    const newModule = new Module(rawModule, runtime)  // 初始化一个新的模块
    if (path.length === 0) {    // 当前没有别的模块
      this.root = newModule     // 则此模块为根模块
    } else {    // 有多个模块     
      const parent = this.get(path.slice(0, -1))   // 获取到新模块从属的父模块,所以是path.slice(0, -1),最后一个元素就是我们要添加的子模块的名称
      parent.addChild(path[path.length - 1], newModule)    // 在父模块中添加新的子模块
    }
    if (rawModule.modules) {     // 如果有嵌套模块
      /**
       *  1. 遍历所有的子模块,并进行注册;
       *  2. 在path中存储除了根模块以外所有子模块的名称
       *  */ 
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}


「函数作用:」


  1. register(path, rawModule, runtime):注册新的模块,并根据模块的嵌套关系,将新模块添加作为对应模块的子模块


  • path:表示模块嵌套关系。当前为根模块时,没有任何嵌套关系,此时 path = [] ; 当前不是根模块时,存在嵌套关系,例如上述例子中的 ModuleA1 ,它是 ModuleA 的子模块 ,而 ModuleA 又是根模块的子模块,此时 path = ['ModuleA', 'ModuleA1']


  • rawModule:表示模块对象,此时是一个对象类型


  • runtime:表示程序运行时


  1. get(path):根据传入的 path 路径,获取到我们想要的 Module

ModuleCollection 的构造函数中调用了 register 函数,前两个参数分别为:[]rawRootModule ,此时肯定是从根模块开始注册的,所以 path 里无内容,并且 rawRootModule 指向的是根模块


然后来看一下 register 函数里的逻辑。


  1. 首先将当前要注册的模块生成一个 Module ,并将 rawModule 作为参数,用于存放 Module 的信息


  1. 然后通过 if(path.length === 0) 判断是否为根模块,是的话就将 this.root 指向 Module ; 否则就跳到第3步


  1. 判断当前模块不是根模块,就通过 get 函数找到当前模块的父模块,然后调用父模块中的 addChild 方法将当前模块添加到子模块中


  1. 最后再判断当前模块是否还有嵌套的模块,有的话就重新回到第1步进行递归操作 ; 否则不做任何处理


按照上面的逻辑,就可以将所有的模块递归收集并注册好了,其中有一个 Module 类还没有具体提到,所以这里移步到 ./module/module.js


import { forEachValue } from '../util'
// 定义了Vuex中的 Module 类,包含了state、mutations、getters、actions、modules
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)   // 创建一个空对象,用于存放当前模块的子模块
    this._rawModule = rawModule         // 当前模块的一些信息,例如:state、mutations、getters、actions、modules
    const rawState = rawModule.state    // 1. 函数类型 => 返回一个obj对象; 2. 直接获取到obj对象
    // 存储当前模块的state状态
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}   
  }
  // 判断该模块是否定义了namespaced,定义了则返回true; 否则返回false
  get namespaced () {
    return !!this._rawModule.namespaced
  }
  // 添加子模块,名称为key
  addChild (key, module) {
    this._children[key] = module
  }
  // 移除名称为key的子模块
  removeChild (key) {
    delete this._children[key]
  }
  // 获取名称为key的子模块
  getChild (key) {
    return this._children[key]
  }
  // 是否存在名称为key的子模块
  hasChild (key) {
    return key in this._children
  }
  // 将当前模块的命名空间更新到指定模块的命名空间中,并同时更新一下actions、mutations、getters的调用来源
  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)
    }
  }
}


来看一下刚才模块收集时,创建的 Module 类内部做了什么事情,同样的从 constructor 中开始看


this._children 是一个对象值,用于存放该模块嵌套的其它 Module 类 ;


this._rawModule 就是用于存放该模块内部的一些信息,例如:statemutationsactionsgettersmoudles ;


this.state 对应的就是 this._rawModule 中的 state ;


这是整个构造函数中执行的操作,我们可以看到,在生成一个 Module 类的时候,其只定义了 state 属性,而 mutationsgettersactionsmodules 都是没有被定义的,即例如现在是无法通过 Module.mutations 获取到该模块所有的

mutations 方法,那么这些方法都是在何时被定义的呢?自然是等模块全部都收集完毕以后才进行的操作,因为 vuex 中的嵌套模块可能会存在命名空间 namespaced


3.2 注册模块


到此为止,各个模块的类都创建好了,那么继续回到 ./src/store.jsconstructor 构造函数中


// 将 dispatch 和 commit 方法绑定到 Store 的实例上,避免后续使用dispatch或commit时改变了this指向
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}
// 判断store是否未严格模式。true: 所有的state都必须经过mutations来改变
this.strict = strict
// 将根模块的state赋值给state变量
const state = this._modules.root.state


这段代码首先对 Store 实例上的 dispatchcommit 方法进行了一层包装,即通过 call 将这两个方法的作用对象指向当前的 Store 实例,这样就能防止后续我们操作时,出现 this.$store.dispatch.call(obj, 1) 类似的情况而报错


this.strict 是用于判断是否是严格模式。因为 vuex 中,建议所有的 state 变量的变化都必须经过 mutations 方法,因为这样才能被 devtool 所记录下来,所以在严格模式下,未经过 mutations 而直接改变了 state 的值,开发环境下会发出警告⚠️


const state = this._modules.root.state  获取的是根模块的 state ,用于后续的一些操作


一切都准备就绪了,下面就开始为每个模块注册信息了


// 从根模块开始,递归完善各个模块的信息
installModule(this, state, [], this._modules.root)


调用了 installModule 方法,并将 store 实例对象 、state 属性 、路径 、根模块对象依次作为参数进行传递


// 注册完善各个模块内的信息
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length  // 是否为根模块
  const namespace = store._modules.getNamespace(path)  // 获取当前模块的命名空间,格式为:second/ 或 second/third/
  // 如果当前模块设置了namespaced 或 继承了父模块的namespaced,则在modulesNamespaceMap中存储一下当前模块
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }
  // 如果不是根模块,将当前模块的state注册到其父模块的state上
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父模块的state
    const moduleName = path[path.length - 1]   // 当前模块的名称
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      // 将当前模块的state注册在父模块的state上,并且是响应式的
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 设置当前模块的上下文context
  const local = module.context = makeLocalContext(store, namespace, path)
  // 注册模块的所有mutations
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key     // 例如:first/second/mutations1
    registerMutation(store, namespacedType, mutation, local)
  })
  // 注册模块的所有actions
  module.forEachAction((action, key) => {
    /**
     * actions有两种写法:
     * 
     * actions: {
     *    AsyncAdd (context, payload) {...},   // 第一种写法
     *    AsyncDelete: {                       // 第二种写法
     *      root: true,
     *      handler: (context, payload) {...}
     *    } 
     * }
     */
    const type = action.root ? key : namespace + key   // 判断是否需要在命名空间里注册一个全局的action
    const handler = action.handler || action          // 获取actions对应的函数
    registerAction(store, type, handler, local)   
  })
  // 注册模块的所有getters
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // 递归注册子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}


const namespace = store._modules.getNamespace(path) 是将路径 path 作为参数, 调用 ModuleCollection 类实例上的 getNamespace 方法来获取当前注册对象的命名空间的


/**
* 根据模块是否有命名空间来设定一个路径名称
* 例如:A为父模块,B为子模块,C为子孙模块
* 1. 若B模块命名空间为second,C模块未设定命名空间时; C模块继承了B模块的命名空间,为 second/
* 2. 若B模块未设定命名空间, B模块命名空间为third; 则此时B模块继承的是A模块的命名空间,而C模块的命名空间路径为 third/
*/
getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)   // 获取子模块
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}


从这可以看出,未指定命名空间的模块会继承父模块的命名空间


// 如果当前模块设置了namespaced 或 继承了父模块的namespaced,则在modulesNamespaceMap中存储一下当前模块
if (module.namespaced) {
  if (store._modulesNamespaceMap[namespace] && __DEV__) {
    console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
  }
  store._modulesNamespaceMap[namespace] = module
}


这段代码是将所有存在命名空间的模块记录在 store._modulesNamespaceMap 中,便于之后的辅助函数可以调用(这里还未提到辅助函数,可以先不管,到时候回头来看)


3.2.1 注册模块的state


// 如果不是根模块,将当前模块的state注册到其父模块的state上
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父模块的state
  const moduleName = path[path.length - 1]   // 当前模块的名称
  store._withCommit(() => {
    if (__DEV__) {
      if (moduleName in parentState) {
        console.warn(
          `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
        )
      }
    }
    // 将当前模块的state注册在父模块的state上,并且是响应式的
    Vue.set(parentState, moduleName, module.state)
  })
}


这段代码主要是将非根模块的 state 挂载到父模块的 state


const parentState = getNestedState(rootState, path.slice(0, -1)) 根据当前的模块路径,从根模块的 state 开始找,最终找到当前模块的父模块的 state,可以看一下 getNestedState 方法内部的具体实现


// 获取到嵌套的模块中的state
function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}


const moduleName = path[path.length - 1] 从路径 path 中将当前模块的名称提取出来


store._withCommit(() => {
  if (__DEV__) {
    if (moduleName in parentState) {
      console.warn(
        `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
      )
    }
  }
  // 将当前模块的state注册在父模块的state上,并且是响应式的
  Vue.set(parentState, moduleName, module.state)
})


这段代码中最主要的部分就是 Vue.set(parentState, moduleName, module.state) ,作用就是调用了 Vueset 方法将当前模块的 state 响应式地添加到了父模块的 state 上,这是因为在之后我们会看到 state 会被放到一个新的 Vue 实例的 data 中,所以这里不得不使用 Vueset 方法来响应式地添加


同样的,从这段代码中我们也可以知道了为什么平时在获取子模块上 state 的属性时,是通过 this.$store.state.ModuleA.name 这样的形式来获取的了


3.2.2 生成模块调用上下文


// 设置当前模块的上下文context
const local = module.context = makeLocalContext(store, namespace, path)


这行代码也可以说是非常核心的一段代码了,它根据命名空间为每个模块创建了一个属于该模块调用的上下文,并将该上下文赋值了给了该模块的 context 属性


接下来看一下这个上下文是如何创建的吧


// 若设置了命名空间则创建一个本地的commit、dispatch方法,否则将使用全局的store
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''  
  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {  // 若传入了第三个参数设置了root:true,则派发的是全局上对应的的actions方法
        type = namespace + type
        if (__DEV__ && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }
      return store.dispatch(type, payload)
    },
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {   // 若传入了第三个参数设置了root:true,则派发的是全局上对应的的mutations方法
        type = namespace + type
        if (__DEV__ && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }
      store.commit(type, payload, options)
    }
  }
  /**
   * 若没有设定命名空间,则直接读取store.getters(store.getters已经挂载到vue实例的computed上了);
   * 若设定了命名空间,则从本地缓存_makeLocalGettersCache中读取getters
   */
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters    
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })
  return local
}


local 这个变量存储的就是一个模块的上下文。


先来看其第一个属性 dispatch ,当该模块没有设置命名空间时,调用该上下文的 dispatch 方法时会直接调用 sotre.dispatch ,即调用了根模块的 dispatch 方法 ; 而存在命名空间时,会先判断相应的命名空间,以此来决定调用哪个 dispatch 方法


if (!options || !options.root) 是判断调用 dispatch 方法时有没有传入第三个参数 {root: true} ,若有则表示调用全局根模块上对应的的 dispatch 方法


那么同样的,local 中的 commit 属性就类似于 dispatch ,这里就不多说了


然后最后通过 Object.defineProperties 方法对 localgetters 属性和 state 属性设置了一层获取代理,等后续对其访问时,才会进行处理。例如,访问 getters 属性时,先判断是否存在命名空间,若没有,则直接返回 store.getters ; 否则的话,根据命名空间创建一个本地的 getters 缓存,根据这个缓存来获取对应的 getters ,来看一下代码


// 创建本地的getters缓存
function makeLocalGetters (store, namespace) {
  // 若缓存中没有指定的getters,则创建一个新的getters缓存到__makeLocalGettersCache中
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {}
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type => {
      // 如果store.getters中没有与namespace匹配的getters,则不进行任何操作
      if (type.slice(0, splitPos) !== namespace) return
      // 获取本地getters名称
      const localType = type.slice(splitPos)
      // 对getters添加一层代理
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true
      })
    })
    // 把代理过的getters缓存到本地
    store._makeLocalGettersCache[namespace] = gettersProxy
  }
  return store._makeLocalGettersCache[namespace]
}


当存在命名空间时访问 local.getters ,首先会去 store._makeLocalGettersCache 查找是否有对应的 getters 缓存,若没有,则创建一个 gettersProxy ,在 store.getters 上找到对应的 getters ,然后用 Object.definePropertygettersProxy 做一层处理,即当访问 local.getters.func 时,相当于访问了 store.getters['first/func'] ,这样做一层缓存,下一次访问该 getters 时,就不会重新遍历 store.getters 了 ; 若有缓存,则直接从缓存中获取


上下文已经创建好了,接下来就是注册 mutationsactionsgetters


3.2.3 注册模块的mutations


// 注册模块的所有mutations
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key     // 例如:first/second/mutations1
  registerMutation(store, namespacedType, mutation, local)
})


这里遍历了模块的所有 mutations 方法,通过命名空间 + mutations 方法名的形式生成了 namespacedType


然后跳到 registerMutations 方法看看具体是如何注册的


// 注册mutations方法
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])  // 通过store._mutations 记录所有注册的mutations
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}


首先根据我们传入的 type 也就是上面的 namespacedTypestore._mutations 寻找是否有入口 entry ,若有则直接获取 ; 否则就创建一个空数组用于存储 mutations 方法


在获取到 entry 以后,将当前的 mutations 方法添加到 entry 末尾进行存储。其中 mutations 接收的参数有两个,即 上下文中的 state 和 我们传入的参数 payload


从这段代码我们可以看出,整个 store 实例的所有 mutations 方法都是存储在 store._mutations 中的,并且是以键值对的形式存放的,例如:


store._mutations = {
  'mutations1': [function handler() {...}],
  'ModuleA/mutations2': [function handler() {...}, function handler() {...}],
  'ModuleA/ModuleB/mutations2': [function handler() {...}]
}


其中「键」是由命名空间和 mutations 方法名组成的,「值」是一个数组,存放着所有该键对应的 mutations 方法


为什么是用数组存放呢?因为在上面说过,假设父模块ModuleA 里有一个叫 funcmutations 方法,那么其在 store._mutations 中就是这个样子的


store._mutations = {
  'ModuleA/func': [function handler() {...}]
}


若子模块没有设置命名空间,那么他是会继承父模块的命名空间的,此时子模块里也有一个叫 funcmutations 方法,那么在获取 entry 时,获取到的是 store._mutations['ModuleA/func'] ,但此时这个 entry 中已经有一个 mutations 方法了,那么为了保证之前的方法不被替换,就选择添加到数组的末尾,此时应该就可以猜测到了,后续如果调用该 mutations 方法,会先获取到相应的数组,然后遍历依次执行


得出个「结论」mutations 方法是可以重名的

相关文章
|
4月前
|
存储 前端开发 JavaScript
PixiJS源码分析系列: 第一章 从最简单的例子入手
PixiJS源码分析系列: 第一章 从最简单的例子入手
|
7月前
|
测试技术
面试题8: 如何确定测试需求的关键场景和细节?
面试题8: 如何确定测试需求的关键场景和细节?
|
前端开发 NoSQL 数据库
项目重点知识点详解
项目重点知识点详解
|
设计模式 存储 安全
Java的第十三篇文章——JAVA多线程(后期再学一遍)
Java的第十三篇文章——JAVA多线程(后期再学一遍)
|
域名解析 网络协议 Oracle
Java的第十五篇文章——网络编程(后期再学一遍)
Java的第十五篇文章——网络编程(后期再学一遍)
|
Java API 容器
java项目设计与思路
与其和大多数Java教程一样,先讲变量,再说继承和多态,再讲数组。还不如直接来看看,我们学习java,能做些什么? 我是小白,这天,就在我慢吞吞地学习Java知识的时候,老板找到我。
100 0
|
移动开发 JSON 小程序
【小程序开篇】小程序架构和配置
【小程序开篇】小程序架构和配置
292 0
【小程序开篇】小程序架构和配置
|
Rust Kubernetes 测试技术
Krustlet 入手案例
本文将对基于 Kind 部署 Krustlet 并实践 Demo 应用
417 0
|
IDE 测试技术 API
聊聊我的源码阅读方法
本次代码阅读的项目来自 500lines 的子项目 web-server。 500 Lines or Less不仅是一个项目,也是一本同名书,有源码,也有文字介绍。这个项目由多个独立的章节组成,每个章节由领域大牛试图用 500 行或者更少(500 or less)的代码,让读者了解一个功能或需求的简单实现。
164 0
聊聊我的源码阅读方法
|
XML 缓存 JSON
看SpringCloudEureka源码前懂得这些知识事半功倍
看SpringCloudEureka源码前懂得这些知识事半功倍
看SpringCloudEureka源码前懂得这些知识事半功倍
下一篇
DataWorks