0%

vuex mapState 函数源码浅析

为了加深对vuex的理解,今天下午的时候刚好有空,所以就挑了vuex的辅助函数 mapState 的源码简单分析了下,顺便写这篇博客记录下来,在分析源码之前,我们先来回顾一下mapState的用法。对于使用过vuex的朋友来说,应该都知道mapState可以将store中的state映射为vue组件的计算属性,通过使用mapState可以减少代码的重复和冗余:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在单独构建的版本中辅助函数为 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
}
})
}

上述代码将count和countPlusLocalState映射为组件的计算属性,如果组件的计算属性跟state的子节点的名称相同时,也可以给mapState传入一个字符串数组:

1
2
3
4
5
6
7
8
9
10
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
// ...
computed: mapState([
'count',
'coutPlusLocalState'
])
}

回顾了mapState的具体用法,接下来就分析一下它的源码吧,vuex辅助函数的源码地址在这里:https://github.com/vuejs/vuex/blob/dev/src/helpers.js这里主要分析mapState函数的源码,其他辅助函数的源码也差不多,分析懂了其中一个,其他也就信手拈来了。需要读懂mapState的源码,必须要有以下三个基础知识点的储备,如果自己忘记了或者记忆不太清了,可以自行去温习一下:

下面我把mapState源码相关的代码先抽出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 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 = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})


/**
* 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.
* @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)
}
}

**
* 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) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}

/**
* Search a special module from store by namespace. if module not exist, print error message.
* @param {Object} store
* @param {String} helper
* @param {String} namespace
* @return {Object}
*/
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}

其实上面的代码,除了export 方法 mapState 外的其他函数都是所有辅助函数所共用的。第一步需要先分析normalizeNamespace 方法,从源码可以看到,它传入了一个参数,这个参数是一个函数fn,调用normalizeNamespace 后会return回一个匿名函数,这个匿名函数可以传入两个参数:一个是namespace, 一个是map, 这两个参数其实就是用来接收我们在组件中调用mapState方法时要传入的参数,通常情况下我们的应用没有那么复杂,不需要使用命名空间,所以一般都是只传入一个参数,也就是那些被映射为组件计算属性的一些值,但是有时候我们的应用会比较庞大,为了后期便于维护,我们会使用到vuex 支持的 module,这时就会有命名空间的设置,这种情况下传入的两个参数就要严格按照这个匿名函数定义时给出的参数列表顺序进行传值,第一个参数必须是命名空间,而第二个参数就是需要被映射给组件的值,看看源码:

1
2
3
4
5
6
7
8
9
10
11
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)
}
}

第一步会先判断mapState那里传入的第一个参数是不是字符串,如果不是字符串,说明没有使用命名空间,于是会把传进来的实参赋值给map,然后将namespace设置为空字符串,接下来讲就分析一下没有命名空间时,mapState的整个执行流程,涉及命名空间的请自行去分析,也差不多,仅仅是多了一个作用域。回到前面的例子,给mapState传入一个对象:

1
2
3
4
5
6
7
8
9
export default {
computed: mapState({
count: state => state.count,
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}

经过mapState工具函数的处理后,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default { 
// ...
computed: {
count() {
return this.$store.state.count
},
countAlias() {
return this.$store.state['count']
},
countPlusLocalState() {
return this.$store.state.count + this.localCount
}
}
}

整个处理的过程是这样的,mapState首先对传入的参数调用 normalizeNamespace,也就是 :

1
2
3
4
5
6
7
normalizeNamespace({
count: state => state.count,
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
})

此时typeof namespace !== ‘string’ 为true,所以接来下会执行:

1
2
3
4
5
6
7
8
map = {
count: state => state.count,
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
}
namespace = ''

接下来会把上面的namespace=”作为第一个参数,map={…} 作为第二个参数传入下面这个函数【后面简称整个函数为fn】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
}

然后对传入的参数调用normalizeMap,这个函数的定义如下:

1
2
3
4
5
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}

从源码也可以看出,map是一个数组或者对象,如果map是一个数组,normalizeMap将传入的map数组的每个元素转换成一个{key, val: key}的对象,如果map是一个对象,则通过Object.keys方法遍历这个map对象的 key,把数组里的每个key都转换成一个{key, val: key}的对象,最后都会把转换后的对象数组作为normalizeMap的返回值。然后继续执行mapState的剩余代码,调用了normalizeMap函数后,把前面传入的states转换成了由{key, val: key}构成的数组,接下来就是调用forEach方法遍历这个数组,并构造了一个新的对象res,这个res对象的每个元素都返回一个新的函数mappedState,来看看mappedState函数内部的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}

由于我们传入的namespace是空,所以会直接执行:

1
2
3
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]

首先会判断val是不是一个函数,如果val是一个函数,则直接调用整个函数,并把store上的state和getters作为参数,执行结果作为mappedState的返回值,如果val不是函数,就直接把this.$store.state[val]作为mappedState的返回值。回到前面的例子,我们把

1
2
3
4
5
6
7
8
9
namespace='', 

state={
count: state => state.count,
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
}

传入fn函数后,先经过normalizeMap函数处理后的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
key: 'count', val: state => state.count
},
{
key: 'coutAlias', val: 'count'
},
{
key: 'countPlusLocalState',
val: function countPlusLocalState (state) { return state.count + this.localCount }
}
]

然后使用forEach方法遍历上这个数组的每个元素,判断到当前元素的val是function的,则会执行:

1
return val.call(this, this.$store.state, this.$store.getters)

也就是前面说的val.call(this, this.$store.state, this.$store.getters)的返回值会作为mappedState函数的返回值,这个返回值也就是res对象的某个key对应的value函数的返回值,比如当res的key是’count’时,它的value就是:

1
2
3
function mappedState() {
return this.$store.state.count
}

所以,最终mapState处理后的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default { 
// ...
computed: {
count() {
return this.$store.state.count
},
countAlias() {
return this.$store.state['count']
},
countPlusLocalState() {
return this.$store.state.count + this.localCount
}
}
}

以上只是个人鄙见,研究还不是很深入,所以理解上可能还会有误差,如果有错误的地方,欢迎指正,感激不尽。

  • 本文作者: 前端农民工
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!