在 Vuex 安装在 Vue 的过程中,会把用户定义的store实例赋值给 Vue 实例对象的$store中,所以我们在 Vue 组件中的任意地方使用this.$store调用store的值。但是这样使用可读性并不是很好,这样不好查找在组件的哪些地方,调用了哪些store上的属性和方法。
一般我们都会使用MapXxx语法糖来引入store的属性,这样很好的查找在哪里使用了store的哪些数据,可读性也会更强,也更方便管理。
mapState#
先看下mapState函数的代码,在helpers.js文件中。
export const mapState = normalizeNamespace((namespace, states) => {
const res = {};
normalizeMap(states).forEach(({ key, val }) => {
// ...
});
return res;
});在分析mapState代码时,需要先搞清楚normalizeNamespace和normalizeMap这两个函数的作用,不然会一脸懵逼。
normalizeNamespace规范化参数#
先看normalizeNamespace函数的代码,这个函数的作用是规范用户传入的参数。
function normalizeNamespace(fn) {
return (namespace, map) => {
// ! 命名空间不为字符串。
// ! 比如,传入 root 的值时,没有模块名,是直接传 { a: mutationName } 或者 [ mutationName ]
if (typeof namespace !== "string") {
map = namespace; // ! 把命名空间设置为 map
namespace = ""; // ! 命名空间为空
// ! 命名空间没有以 / 结尾时,拼接 / => moduleName = moduleName/
// ! 模块名和 type 之间需要使用 / 隔开
} else if (namespace.charAt(namespace.length - 1) !== "/") {
namespace += "/";
}
return fn(namespace, map);
};
}这个函数返回的也是一个函数,而且这个函数和我们传入的fn参数具有相同的参数namespace和map,这样就可以很好的约束fn的参数。
首先判断第一个参数namespace是不是字符串,如果不是字符串,那么传入的值就是一个对象或者数组。这时参数namespace就不再是命名空间了,而是把它当成第二个参数map,而第一个参数namespace的值就是空字符串。
如果第一个参数namespace的值是字符串类型,那么它就是命名空间的模块名,这时候判断模块名是不是以/符号结尾,如果不是的话,需要在模块名后面添加/符号,这样做主要是为了在后面更好的拼接type值。
最后再次调用fn函数,调用时把处理好的两个参数namespace和map传入进去。使用normalizeNamespace函数可以很好的规范namespace和map的值。
normalizeMap规范化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}
* ! 规范化 Map 👆
*/
function normalizeMap(map) {
return Array.isArray(map)
? map.map((key) => ({ key, val: key })) // ! 不修改 key 的名字
: Object.keys(map).map((key) => ({ key, val: map[key] })); // ! 映射,修改 key的名字
}这个函数的作用是规范传入map参数的,官方注释已经解释的非常清楚了。它会把数组类型或者对象类型的map转统一换成一个{key, value}结构的对象组成的数组。
比如我们使用mapState时,一般是这样使用的。
computed: {
...mapState(['value1', 'value2']),
...mapState({val3: 'value3', val4: 'val4'}),
...mapState('moduleA', ['value1', 'value2']),
...mapState('moduleB', {val3: 'value3', val4: 'value4'})
}规范map后就变成下面这样子。
computed: {
...mapState([{ key:'value1',val:'value1'}, { key:'value2',val:'value2' }]),
...mapState([{ key: val3: val: 'value3' }, { key: val4: val: 'val4' }]),
...mapState('moduleA/', [{ key:'value1',val:'value1'}, { key:'value2',val:'value2' }]),
...mapState('moduleB/', [{ key: val3: val: 'value3' }, { key: val4: val: 'val4' }])
}现在我们可以再看mapState的代码。
export const mapState = normalizeNamespace((namespace, states) => {
const res = {};
// ! states: [1, 2, 3] => [{ key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 }]
// ! states: {a:1, b:2, c:3} => [{ key: a, val: 1 }, { key: b, val: 2 }, { key: c, val: 3 }]
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState() {
// ! 获取 root 上的值
let state = this.$store.state;
let getters = this.$store.getters;
// ! 如果设置了命名空间,即 mapXXX(namespace, ['name1', 'name2'])
// ! 获取命名空间模块的值,即 store.state.namespace.name1
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) // ! val(state, getters)
: state[val]; // ! 返回 state 中对应的值即可
};
// mark vuex getter for devtools
res[key].vuex = true;
});
return res;
});首先声明了变量res,它的初始值是空对象,最后会返回这个值。然后遍历states,就是上面规范后的map的值。然后在res对象中生成一对对{key: mappedState}键值对结构的数据。
下面主要看下mappedState函数的代码。
首先声明变量state存储 root 上的state,声明变量getters存储 root 上的getters,如果有命名空间的模块后,会获通过命名空间的模块名获取到这个模块,然后变量state和getters会变成模块上下文中的state和getters。
再判断val的值是否是函数,有时候会通过调用函数生成数据。如果是函数的话,会使用call方法来调用,this指向组件自身,然后把state和getters作为参数传入,如果不是函数,一般就是字符串,会返回state中对应val的值。
我们一般使用时传入字符串类型的值多一点,看下面的代码。
mapState("moduleA", ["value"]);这时会返回state[val],然后重新查看在模块上下文中是怎么修改state的值。
Object.defineProperties(local, {
state: {
get: () => getNestedState(store.state, path), // ! 通过路径获取嵌套的 state
},
});也就是说会使用getNestedState方法,通过路径path获取对应的state的值。而这里有命名空间模块,那么path的值就是['moduleA','value'],这样就可以获取到模块中value的值。
mapMutations#
下面看下mapMutations函数的代码。
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {};
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation(...args) {
// Get the commit method from store
let commit = this.$store.commit; // ! 根的 commit
if (namespace) {
// ! 获取模块
const module = getModuleByNamespace(
this.$store,
"mapMutations",
namespace
);
if (!module) {
return;
}
commit = module.context.commit; // ! 模块的 commit
}
return typeof val === "function"
? // ! 调用这个函数 val(commit, args),函数传入 commit,在函数体中可以使用 commit 来提交其它 mutation
val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args)); // ! string 形式 --> this.$store.commit(val, ...args)
};
});
return res;
});逻辑和mapState大同小异,有命名空间模块的时候,获取到的是模块上下文中的commit方法。
然后判断val是否为函数,如何是的话,会调用这个函数,并且传入commit和参数args。这里为什么要传入commit,因为在函数体中可以使用commit来提交其它的mutation函数。如果val不是函数,那应该是字符串,这时会调用commit函数,this指向的this.$store,然后传入val和参数args,这就相当于是下面这样调用的。
this.$store.commit(val, ...args);我们发现mapMutations并不是简单的映射mutation的函数名,而是映射一个包装函数,这个函数里面使用commit来提交mutation函数,用户在组件中使用这个函数就相当于提交mutation函数,而不需要在组件中再次显示使用commit来提交,这样设计是非常人性化的。
mapGetters#
下面看下mapGetters函数的代码。
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {};
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val; // ! moduleName/getterName
res[key] = function mappedGetter() {
if (
namespace &&
!getModuleByNamespace(this.$store, "mapGetters", namespace)
) {
return;
}
if (
process.env.NODE_ENV !== "production" &&
!(val in this.$store.getters)
) {
console.error(`[vuex] unknown getter: ${val}`);
return;
}
return this.$store.getters[val]; // ! 根据拼接后的 val 从实例属性 getters 获取对应的值
};
// mark vuex getter for devtools
res[key].vuex = true;
});
return res;
});mapGetters的实现和mapState有一些不同,主要是Store中getters属性和state属性的结构不一样。
// state
state: {
count: 0 // root state
moduleA: {
count: 1
},
moduleB: {
count: 2
}
}
// getters
getters: {
getterFn(){}, // root getters
'modulesA/getterFn'() {},
'modulesA/getterFn2'() {},
'modulesB/getterFn'() {}
}getters采用字符串拼接的方式,使得val的值为moduleName/getterName,然后在store.getters中直接通过 key 获取值。
mapActions#
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {};
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction(...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch;
if (namespace) {
const module = getModuleByNamespace(
this.$store,
"mapActions",
namespace
);
if (!module) {
return;
}
dispatch = module.context.dispatch;
}
return typeof val === "function"
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args));
};
});
return res;
});mapActions的逻辑和mapMutations完全一样,这样就不多赘述了。
createNamespacedHelpers#
/**
* 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),
});Vuex 还暴露出一个createNamespacedHelpers函数,用来绑定命名空间模块namespace的值,这样后面使用时所有的map都是这个模块下面的值。