vuex 源码分析(7)—— mapState, mapGetters, mapActions 和 mapMutations

842 阅读3分钟

前言

若想快速地理解一个方法的源码,学习并掌握此方法,是不可避免的。快速通道

辅助方法

下面列举的辅助方法非常重要,看懂它们就能看懂mapState, mapGetters等辅助函数的源码。不需用一上来就要搞懂这些方法,但要明白它们的功能作用,结合着辅助函数的源码一起看。

normalizeNamespace

返回一个函数,它包含两个参数:命名空间和映射。它将规范化名称空间,然后返回传入的函数,来处理新的名称空间和映射。

function normalizeNamespace (fn) {
  return (namespace, map) => {
    // 当参数namespace,不是String类型,而是一个对象:'Array' 或 'Object'
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      // namespace的最后一个字符,如果不是 '/',就拼接上
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

isValidMap

验证参数 map 类型:'Array' 或 'Object'。

function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}

isObject

验证参数 obj 是否为 'Object'。

export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

normalizeMap

规范化 map 参数,且 map 必须为:'Array' 或 'Object'。例如:

  1. normalizeMap([1, 2, 3]) => [{key: 1, val: 1}, {key: 2, val: 2}]

  2. normalizeMap({a: 1, b: 2}) => [{key: 'a', val: 1}, {key: 'b', val: 2}]

function normalizeMap (map) {
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

数组方法:

  1. isArray() ,用于确定传递的值是否是一个 Array。

  2. map() ,返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。它按照原始数组元素顺序依次处理元素。

    注意事项:

    • 不会对空数组进行检测。
    • 不会改变原始数组。

getModuleByNamespace

依据 namespace(模块的空间名称),在 store 实例中查找指定的模块。如果模块不存在,则打印错误信息。

function getModuleByNamespace (store, helper, namespace) {
  // _modulesNamespaceMap 是 store 用于存储模块的空间名称的一个对象
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

mapState 辅助函数

源码

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  // __DEV__ 开发环境下,值为:true。
  //  isValidMap 验证参数类型是否为:'Array' 或 'Object'
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      // 默认获取根模块下的state,getters
      let state = this.$store.state
      let getters = this.$store.getters
      // namespace为真,意味着将模块的空间名称字符串作为了mapState函数的第一个参数。
      // 它会让所有绑定都自动将该模块作为上下文。
      if (namespace) {
        // 获取指定模块
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 获取指定模块下的state,getters,以覆盖默认的
        state = module.context.state
        getters = module.context.getters
      }
      // val可以是字符串或函数
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // 为devtools标记vuex getter
    res[key].vuex = true
  })
  return res
})

使用方式

以下所举代码示例,来自vuex官网文档。

  1. 第一个参数(namespace)是对象:'Array' 或 'Object' 类型。
  • 参数是 'Object'
computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
})

'Object'对象中的属性值,可以是一个函数。这是mapState源码中,会对 val 进行判断的原因。看下面的源代码。

// val可以是字符串或函数
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
  • 参数是 'Array'
computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])
  1. 第一个参数(namespace)是字符串:'String' 类型。

namespace为真,意味着将模块的空间名称字符串作为了mapState函数的第一个参数。它会让所有绑定都自动将该模块作为上下文。

还记得上面这句话吗?它是对mapState中部分源码的注释,下面的举例,想必会让你更好地理解这段话。官方文档详细介绍

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
}

mapGetters 辅助函数

源码

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  // __DEV__ 开发环境下,值为:true。
  //  isValidMap 验证参数类型是否为:'Array' 或 'Object'
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // 将namespace和val拼接并赋值给val,用于获取相应的getter。注意,
    // 此时的namespace(命名空间)已被normalizeNamespace函数修改。
    
    val = namespace + val
    res[key] = function mappedGetter () {
      // namespace存在,且获取不到相应的模块,则阻止运行。
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 开发环境下,getters中不存在val,则阻止运行并打印错误信息。
      if (__DEV__ && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      // 获取相应的getter
      return this.$store.getters[val]
    }
    // 为devtools标记vuex getter
    res[key].vuex = true
  })
  return res
})

mapGetters源码并不复杂,唯一需要注意的地方可能是这段代码:

// namespace指命名空间,val指相应的getters属性
val = namespace + val

在上面的源码注释中已说明了它的作用:获取相应的getter。但这要分成两种情况,这里再详细说明一下:

  1. namespace为空字符串(即 '' ),则获取根模块下的getter。
  2. namespace不为空,则获取指定模块下的getter。

若是对这种获取相应getter的方式,仍旧有疑惑,建议重新阅读vuex源码中关于注册getter部分的源码。也就是,下面这段代码(为简便,registerGetter部分的代码未贴出,大家可自行翻阅)。

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

想必你已注意到namespacedType变量,namespace指命名空间,key指相应的getters属性。这就是,怎么存的,就怎么取。

使用方式

由于mapGetters的使用方式类同于mapState,所以这里就不再举例。若是不太清楚,大家可查阅官网文档

mapMutations 辅助函数

源码

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  // __DEV__ 开发环境下,值为:true。
  //  isValidMap 验证参数类型是否为:'Array' 或 'Object'
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(mutations).forEach(({ key, val }) => {
    // mapMutations 支持载荷,所以res[key],也就是 mappedMutation 函数需要接受参数
    res[key] = function mappedMutation (...args) {
      // 从store获取commit方法
      let commit = this.$store.commit
      // 判断 namespace 是否存在
      if (namespace) {
        // 获取指定的模块
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        //若 namespace(命名空间)存在 则从指定的模块上下文中获取commit方法
        commit = module.context.commit
      }
      // 判断 val 是否为函数,因为传入 mapMutations 的参数是 'Object'时,它的属性值
      // 可以是字符串或函数。
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

使用方式

mapMutations使用方式同mapState一样,只不过,它是在methods对象中调用。

methods: {
    ...mapMutations({
      'increment', // 字符串
      // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
      add: 'increment' , // 字符串,起别名
      inc: function (commit, payload) { console.log(commit, payload) }, // 函数
    })
  }

mapActions 辅助函数

源码

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  // __DEV__ 开发环境下,值为:true。
  //  isValidMap 验证参数类型是否为:'Array' 或 'Object'
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(actions).forEach(({ key, val }) => {
    // mapActions 支持载荷,所以res[key],也就是 mappedAction 函数需要接受参数
    res[key] = function mappedAction (...args) {
      // 从store获取dispatch方法
      let dispatch = this.$store.dispatch
      if (namespace) {
        // 获取指定模块
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        //若 namespace(命名空间)存在 则从指定的模块上下文中获取commit方法
        dispatch = module.context.dispatch
      }
      // 判断 val 是否为函数,因为传入 mapActions 的参数是 'Object'时,它的属性值
      // 可以是字符串或函数。
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

使用方式

mapActions使用方式同mapMutations一样,它也是在methods对象中调用。

 methods: {
    ...mapActions({
      'increment' , // 字符串
      // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
      add: 'increment' , // 字符串,起别名
      inc: function (commit, payload) { console.log(commit, payload) }, // 函数
    })
}

createNamespacedHelpers 辅助函数

createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数。其功能作用理解起来可能有点不明所以,但结合其源码和示例看,你就能很容易明白。

源码

export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

使用方式

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'increment'
    ])
  }
}

结束语

辅助函数的代码并不复杂,很是容易理解,尤其是对已经阅读过vuex是如何注册state、getter、action和mutation源码的同学。若是,同学们还没读过相关的vuex源码,不妨看看本人写的vuex解读。最后,求点赞。