Vuex 4源码学习笔记 - mapState、mapGetters、mapActions、mapMutations辅助函数原理(六)

1,077 阅读4分钟

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

在上一篇笔记中:Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)

我们通过dispatch的调用,一步步的看到Vuex的内部工作流程,这也是看源码的最好的方式,只要捋清楚大概主流程后,再去看那些细枝末节就容易多了。

今天我们就来看看这几个Vuex的辅助函数,分别为mapState、mapGetters、mapActions、mapMutations、createNamespacedHelpers,从名字看到,它们是辅助函数,意思就是,不用它们我们也可以使用Vuex,使用它们只不过是让我们更加方便的应用Vuex。

注意:这几个函数并不能使用在Vue3的Compositon API中,一会从下面的代码我们就可以看到,这几个辅助函数内都用到了this来访问当前的组件实例,而在Setup中,this还没有被创建,所以不能使用这些辅助函数。

mapState

使用方式

在组件中,如果我们需要访问Vuex中的,我们可以通过计算属性的方式

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

但当一个组件需要获取多个状态的时候,就会写很多的计算属性,为了解决这个问题Vuex提供了 mapState 辅助函数帮助我们生成计算属性,可以少写一写代码,比如:

import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

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

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

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

同样的,如果组件已经有其他的计算属性,我们也可以使用对象展开运算符进行添加

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

原理探究

首先mapState的代码在src/helpers.js文件中

其实这个代码很简单,也就30行左右,下面我们来看里面做了什么

/**
 * Reduce the code which written in Vue.js for getting the state.
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
 * @param {Object}
 */
export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  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 () {
      // 这个函数相当于我们自己写的计算属性函数
      // 获取vuex中的state
      let state = this.$store.state
      // 获取vuex中的getters
      let getters = this.$store.getters
      if (namespace) {
        // 通过namespace获取module模块
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 获取module的state
        state = module.context.state
        // 获取module的getters
        getters = module.context.getters
      }
      // val.call,这里val就是我们的箭头函数state => state.products.all,函数内的this为当前组件,state为传入的vuex的state
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

normalizeNamespace函数主要是用来统一处理成标准格式的namespace和map数据,然后使用处理的标准数据作为参数调用传入的函数。

/**
 * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
 用来统一处理成标准格式的namespace和map数据,然后使用处理的标准数据作为参数调用传入的函数
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

然后通过normalizeMap函数将数组格式数据和对象格式数据统一处理成标准格式的数据,它的注释已经写的很清楚,入参和返回值。

/**
 * Normalize the map
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Object}
 */
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] }))
}

最后返回的数据格式为一个对象,如下,经过mapState函数封装的函数

{
  products: function mappedState () {/* ... */}
}

mapGetters、mapMutations、mapActions

在看完了mapState的代码后,其实mapGetters、mapMutations、mapActions就都是差不多的了,都是会先走normalizeNamespace函数来统一标准化输入,然后通过normalizeMap函数统一处理成标准格式的数据。然后最后返回一个对象格式的key-value格式的数据,可以让我们使用对象展开运算符进行混入。

createNamespacedHelpers

最后一个辅助函数,我们使用 mapStatemapGettersmapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

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

通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

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

那么createNamespacedHelpers函数的原理也十分简单,4行代码,通过每个函数的bind方法,十分的巧妙的绑定了namespace参数。

/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}
 */
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

今天我们通过mapState来看到了它的实现原理,其他几个函数也和它差不多,我们可以通过断点的方式一步步看到它的执行流程。

一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新