vuex 源码分析(3)—— installModule 安装模块

462 阅读5分钟

相关的util工具方法

  1. isObject方法,判断obj参数是否是一个对象。
export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

installModule 参数介绍

  1. store:指store对象实例
  2. rootState:根模块的state
  3. path:按序(模块嵌套层级)存储模块名的数组。上一章,注册模块有它的介绍。
  4. module:模块对象
  5. hot:布尔值,同内部变量isRoot一起,用于控制是否使用Vue.set设置state
function installModule(store, rootState, path, module, hot) {
    // ...
}

installModule 函数逐步分析

定义变量 isRoot 和 namespace

  • installModule一旦调用,就会先在函数顶部定义两个变量isRoot和namespace。
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
  1. isRoot是一个布尔值。若是根模块(path.length = 0),则值为true。若是非根模块(path.length > 0),则值为false。

  2. namespace是一个字符串。通过调用模块的getNamespace方法返回的,由模块名和'/'拼接而成的字符串。例如:'moduleB/' 或 'moduleB/moduleA/'。关于这个方法的讲解,在上一章中已配合案例做出介绍。

在名称空间映射中注册模块

_modulesNamespaceMap 是初始化store实例时定义的一个对象,可理解为——模块名称空间映射表。它专门用来存储带命名空间的模块。

// module.namespaced 为true,则表明此模块开启了命名空间。
// 就是在定义模块时,设置了 namespaced: true的模块。
if (module.namespaced) {
    // __DEV__ 开发环境下为true
    // namespace是否已存在于 _modulesNamespaceMap 中
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
}

下图展示的是存储完成后的 _modulesNamespaceMap 对象。 c1.jpg

仔细阅读过vuex文档的同学们,应该会很容易理解_modulesNamespaceMap的作用。它同 mapStatemapGetters, mapActionsmapMutations 这些函数有所关联。这些函数的源码都会用到这个对象。当使用这些函数来绑定带命名空间的模块时,为写起来方便,可将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。下面的示例引用自vuex官网文档

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

设置 state 对象

这个state指的是根模块的state(this._modules.root.state)。 c2.jpg

isRoot 和 hot的值必须同时为false(取反为true),才能设置state。

 if (!isRoot && !hot) {
    // 获取父模块的 state。rootState是根模块的 state,path.slice(0, -1)返回一个
    // 数组,这个数组不包含 path 数组的最后一位元素。
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 子模块名称
    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)
    })
  }

可以看到,在使用Vue.set给父模块的state对象添加新属性时,是在store实例的 _withCommit 方法中调用的。

_withCommit(fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

这个函数很简单。它先是保存_committing(布尔值,初始化时定义,默认false)的原始值,接着将其设置为true,然后执行fn函数(调用_withCommit时传入的函数,一般都是修改state),当函数执行完毕后,再将_committing设置为原来的值。

可是,这么做有何意义呢?看下面这段代码。

function enableStrictMode(store) {
  store._vm.$watch(function () {
    return this._data.$$state
  }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, {
    deep: true,
    sync: true
  })
}

只要开启严格模式,enableStrictMode方法就会被调用。这个方法用Vue中的$watch对this._data.$$state(它指向rootState)进行监听,一旦修改了rootState中的状态且store._committing的值又为false时,它就会抛错。初始化时,默认_committing为false。

但,在源代码中,修改状态是不可避免的。因此,为了防止报错,就把对修改状态的操作,放入_withCommit中执行,因为它会在修改状态前,把_committing设置为true,等修改完成后,再把_committing还原。so,明白否!

makeLocalContext 创建模块上下文环境

const local = module.context = makeLocalContext(store, namespace, path)

makeLocalContext会为每个module生成对应的上下文环境。

c3.jpg

其中定义了dispatch、commit、getters和state属性。下面是makeLocalContext函数的源码。

function makeLocalContext(store, namespace, path) {
  // 判断命名空间路径是否存在
  const noNamespace = namespace === ''
  // 在local对象中定义dispatch、commit、getters和state,并返回local
  const local = {
    // 根据 namespace 判断。若是true,就直接调用store.dispatch,否则就先
    // 对参数进行处理,然后才调用store.dispatch
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      // unifyObjectStyle 根据_type参数类型对接受的三个参数进行调整(下面会细说)
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options} = args
      let { type } = args
      
      // options.root为true时,将允许在命名空间模块里分发根的 action,否则只分发
      // 当前模块内的action
      
      // options不存在(null或undefined)或 options.root为false,则将命名空间路径
      // 与 type(action函数名)进行拼接。拼接后的字符串:'moduleB/increment',作用
      // 就是可以这样调用:this.$store.dispatch('moduleB/increment')
      if (!options || !options.root) {       
        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)
    },
    // 同dispatch处理方式一样
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        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)
    }
  }
  
  // 给local对象添加getters和state属性
  // getter和state对象必须惰性获取,因为它们将被vm更新更改
  Object.defineProperties(local, {
    getters: {
      get: noNamespace ?
        () => store.getters : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
  • unifyObjectStyle 方法

在解读这个方法之前,我们需要了解以下两点(均摘自官网):

  1. Actions 支持载荷方式和对象方式进行分发
// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
  1. dispatch 使用介绍

分发 action。options 里可以有 root: true,它允许在命名空间模块里分发根的 action。 返回一个解析所有被触发的 action 处理器的 Promise。

dispatch(type: string, payload?: any, options?: Object): Promise<any>

dispatch(action: Object, options?: Object): Promise<any>

看出来了吗?从上面列出的两点内容,我们可以清楚的知道,action支持两种形式进行分发,且分发时传入的第一个参数类型并不一致。它可以是 String 或 Object。

为了处理这种情况,unifyObjectStyle方法就应运而生了。看下面代码。

function unifyObjectStyle(type, payload, options) {
  // type是对象且type.type为真,则是以对象形式分发,需要要对参数进行调整,否则是载荷形式
  // 分发。且对参数的调整是以载荷形式为准。
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  // __DEV__ 开发环境下为true
  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }
  // 将参数放在一个对象中返回。
  return {
    type,
    payload,
    options
  }
}
  • makeLocalGetters 方法
// 缓存getters
function makeLocalGetters(store, namespace) {
  // 是否存在缓存
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {} // 可理解为getters的代理对象
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type => {
      // 示例(仅为举例):type ——> 'moduleB/increment'

      // 如果目标getter的名称空间与namespace不匹配,则跳过。
      // 注意:此段代码和registerGetter方法结合看会更容易理解store.getters
      // 中的属性即type,为什么会是:'moduleB/increment'(仅为举例),这种形式。
      if (type.slice(0, splitPos) !== namespace) return

      // 提取getter函数名
      const localType = type.slice(splitPos)
      
      // 为gettersProxy对象定义属性
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true // 可枚举
      })
    })
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}
  • getNestedState 方法

获取模块相应的state,此方法的核心在于对数组方法reduce的使用。

function getNestedState(state, path) {
  return path.reduce((state, key) => state[key], state)
}

注册mutations、actions 和 getters

关于注册module的mutations、actions和getters这一部分的源码并不复杂,这里不在不在一一介绍,所以同学们要自己尝试调试一下哟。

// 注册module中的mutations、actions和getters,并将其this绑定到当前store对象
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

注册子模块

使用模块中定义的forEachChild方法,遍历子模块进行注册。

 module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })

结束语

虽然没有详尽的去解释代码,但不妨碍这文章还是有点东西的。啊哈哈。。。

c4.jpg