在第一章咱们曾经说过:html
VUEX采用的是典型的IIFE(当即执行函数表达式)模式,当代码被加载(经过<script>
或Vue.use()
)后,VUEX会返回一个对象,这个对象包含了Store
类、install
方法、mapState
辅助函数、mapMutations
辅助函数、mapGetters
辅助函数、mapActions
辅助函数、createNamespacedHelpers
辅助函数以及当前的版本号version
。
本章就将详细讲解mapState
、mapMutations
、mapGetters
、mapActions
、createNamespacedHelpers这5个辅助和函数。
vue
若是你在使用VUEX过程当中使用过mapState
辅助函数将state映射为计算属性你应该会为它所支持的多样化的映射形式感到惊讶。咱们不妨先来看看官方文档对它的介绍:git
若是你深刻思考过你可能会有疑问:VUEX的mapState
是如何实现这么多种映射的呢?若是你如今还不明白,那么跟随咱们来一块儿看看吧!github
mapState
辅助函数定义在VUEX源码中的790 ~ 815 行,主要是对多种映射方式以及带命名空间的模块提供了支持,咱们来看看它的源码:vuex
var mapState = normalizeNamespace(function (namespace, states) { var res = {}; normalizeMap(states).forEach(function (ref) { var key = ref.key; var val = ref.val; res[key] = function mappedState () { var state = this.$store.state; var getters = this.$store.getters; if (namespace) { var 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 });
能够看到,mapState函数其实是以函数表达式的形式的形式定义的,它的实际函数是normalizeNamespace函数,这个函数会对mapState函数的输入参数进行归一化/规范化处理,其最主要的功能是实现了支持带命名空间的模块,咱们来看一下它的实现:数组
function normalizeNamespace (fn) { return function (namespace, map) { if (typeof namespace !== 'string') { map = namespace; namespace = ''; } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/'; } return fn(namespace, map) } }
能够看到mapState其实是中间的那段函数:promise
return function (namespace, map) { if (typeof namespace !== 'string') { map = namespace; namespace = ''; } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/'; } return fn(namespace, map) }
它实际接收namespace, map能够接收两个参数,也能够只接受一个map参数。app
由以上分析咱们能够知道,上述官方文档在此处的示例其实并不完善,该实例并无指出能够经过提供模块名称做为mapState的第一个参数来映射带命名空间的模块的state。函数
咱们举个例子看一下:学习
const moduleA = { namespaced: true,//带命名空间 state: { count1: 1, age1: 20 } } const store = new Vuex.Store({ state() { return { count: 0, age: 0 } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, computed: Vuex.mapState('a', {// 映射时提供模块名做为第一个参数 count1: state => state.count1, age1: state => state.age1, }) }) console.log(vm)
其输出以下:
传递模块名称后,咱们只能映射带命名空间的该模块的state,若是该模块不带命名空间(即没有设置namespace属性)、或者对于其它名字的模块,咱们是不能映射他们的state的。
传递了模块名称,但该模块不带命名空间,尝试对其进行映射:
const moduleA = { // namespaced: true, state: { count1: 1, age1: 20 } } const store = new Vuex.Store({ state() { return { count: 0, age: 0 } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, computed: Vuex.mapState('a', { count1: state => state.count1, age1: state => state.age1, }) }) console.log(vm)
传递了模块名称,但尝试映射其它模块的state:
const moduleA = { namespaced: true, state: { count1: 1, age1: 20 } } const store = new Vuex.Store({ state() { return { count: 0, age: 0 } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, computed: Vuex.mapState('a', { count1: state => state.count, age1: state => state.age, }) }) console.log(vm)
这两种状况下的输出结果都会是undefined:
讲完了mapState的参数,咱们接着回过头来看看mapState的实现。这里重复粘贴一下前面有关mapState定义的代码:
function normalizeNamespace (fn) { return function (namespace, map) { if (typeof namespace !== 'string') { map = namespace; namespace = ''; } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/'; } return fn(namespace, map) } }
咱们能够看到,在归一化/规范化输入参数后,mapState函数其实是返回了另一个函数的执行结果:
return fn(namespace, map)
这个fn
就是以函数表达式定义mapState函数时的normalizeNamespace 函数的参数,咱们在前面已经见到过。再次粘贴其代码以便于分析:
function (namespace, states) { var res = {}; normalizeMap(states).forEach(function (ref) { var key = ref.key; var val = ref.val; res[key] = function mappedState () { var state = this.$store.state; var getters = this.$store.getters; if (namespace) { var 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 };
粗略来看,这个函数会从新定义map对象的key-value对,并做为一个新的对象返回。咱们来进一步具体分析一下。
该函数首先调用normalizeMap函数对state参数进行归一化/规范化。normalizeMap函数定义在VUEX源码的899 ~ 903行,咱们来具体看看它的实现:
function normalizeMap (map) { return Array.isArray(map) ? map.map(function (key) { return ({ key: key, val: key }); }) : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); }) }
该函数实际上意味着mapState函数的map参数同时支持数组和对象两种形式。
这两种形式最终都会获得一个新数组,而数组元素就是{key: value}形式的对象。
这也与官方文档的描述相印证,官方文档的既提供了mapState函数的map参数是对象的例子,也提供了参数是数组的例子。
回过头来看,normalizeMap(states)函数执行完后会遍历,针对每个对象元素的value作进一步的处理。它首先拿的是根实例上挂载的store模块的state:
var state = this.$store.state; var getters = this.$store.getters;
而若是mapState函数提供了命名空间参数(即模块名),则会拿带命名空间模块的state:
if (namespace) { var module = getModuleByNamespace(this.$store, 'mapState', namespace); if (!module) { return } state = module.context.state; getters = module.context.getters; }
这其中会调用一个从根store开始,向下查找对应命名空间模块的方法getModuleByNamespace,它定义在VUEX源码的917 ~ 923 行:
function getModuleByNamespace (store, helper, namespace) { var module = store._modulesNamespaceMap[namespace]; if ("development" !== 'production' && !module) { console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace)); } return module }
由于咱们在实例化Store类的时候已经把全部模块以namespace的为key的形式挂载在了根store实例的_modulesNamespaceMap属性上,因此这个查询过程只是一个对象key的查找过程,实现起来比较简单。
回过头来继续看mapState函数中“normalizeMap(states)函数执行完后会遍历,针对每个对象元素的value作进一步的处理
”的最后的执行,它会根据原始的value是不是function而进一步处理:
第二种状况在前述官方文档的例子中也有所体现:
// 为了可以使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount }
但这个官方文档例子并不完整,它并无体现出还会暴露出getters参数,实际上,上述例子的完整形式应该是这样子的:
// 为了可以使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state, getters) { return state.count + this.localCount + getters.somegetter }
与mapState能够映射模块的state为计算属性相似,mapMutations也能够将模块的mutations映射为methods,咱们来看看官方文档的介绍:
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ // 将 `this.increment()` 映射为 `this.$store.commit('increment')` 'increment', // `mapMutations` 也支持载荷: // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)` 'incrementBy' ]), ...mapMutations({ add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')` }) } }
一样咱们来看看它是如何实现的,它的实现定义在VUEX源码中的817 ~ 841 行:
var mapMutations = normalizeNamespace(function (namespace, mutations) { var res = {}; normalizeMap(mutations).forEach(function (ref) { var key = ref.key; var val = ref.val; res[key] = function mappedMutation () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var commit = this.$store.commit; if (namespace) { var module = getModuleByNamespace(this.$store, 'mapMutations', namespace); if (!module) { return } commit = module.context.commit; } return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) }; }); return res });
和mapState的实现几乎彻底同样,惟一的差异只有两点:
咱们来具体分析一下代码的执行:
首先是拷贝载荷:
var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ];
而后是拿commit,若是mapMutations函数提供了命名空间参数(即模块名),则会拿带命名空间模块的commit:
var commit = this.$store.commit; if (namespace) { var module = getModuleByNamespace(this.$store, 'mapMutations', namespace); if (!module) { return } commit = module.context.commit; }
最后则会看对应mutation的value是否是函数:
也就是说,官方文档例子并不完整,它并无体现第二种状况,实际上,官方文档例子的完整形式还应当包括:
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations('moduleName', { addAlias: function(commit, playload) { //将 `this.addAlias()` 映射为 `this.$store.commit('increment', amount)` commit('increment') //将 `this.addAlias(playload)` 映射为 `this.$store.commit('increment', playload)` commit('increment', playload) } }) } }
一样,mapMutations上述映射方式都支持传递一个模块名做为命名空间参数,这个在官方文档也没有体现:
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations('moduleName', [ // 将 `this.increment()` 映射为 `this.$store.commit('increment')` 'increment', // `mapMutations` 也支持载荷: // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)` 'incrementBy' ]), ...mapMutations('moduleName', { // 将 `this.add()` 映射为 `this.$store.commit('increment')` add: 'increment' }), ...mapMutations('moduleName', { addAlias: function(commit) { //将 `this.addAlias()` 映射为 `this.$store.commit('increment')` commit('increment') } }) } }
咱们能够举个例子证实一下:
const moduleA = { namespaced: true, state: { source: 'moduleA' }, mutations: { increment (state, playload) { // 这里的 `state` 对象是模块的局部状态 state.source += playload } } } const store = new Vuex.Store({ state() { return { source: 'root' } }, mutations: { increment (state, playload) { state.source += playload } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, mounted() { console.log(this.source) this.localeincrement('testdata') console.log(this.source) }, computed: Vuex.mapState([ 'source' ] ), methods: { ...Vuex.mapMutations({ localeincrement (commit, args) { commit('increment', args) } }) } })
输出结果:
root test.html:139 roottestdata
另一个例子:
const moduleA = { namespaced: true, state: { source: 'moduleA' }, mutations: { increment (state, playload) { // 这里的 `state` 对象是模块的局部状态 state.source += playload } } } const store = new Vuex.Store({ state() { return { source: 'root' } }, mutations: { increment (state, playload) { state.source += playload } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, mounted() { console.log(this.source) this.localeincrement('testdata') console.log(this.source) }, computed: Vuex.mapState('a', [ 'source' ] ), methods: { ...Vuex.mapMutations('a', { localeincrement (commit, args) { commit('increment', args) } }) } })
输出结果:
moduleA test.html:139 moduleAtestdata
与mapState能够映射模块的state为计算属性相似,mapGetters也能够将模块的getters映射为计算属性,咱们来看看官方文档的介绍:
mapGetters辅助函数定义在VUEX源码中的843 ~ 864 行,咱们来看看它的源码:
var mapGetters = normalizeNamespace(function (namespace, getters) { var res = {}; normalizeMap(getters).forEach(function (ref) { var key = ref.key; var val = ref.val; val = namespace + val; res[key] = function mappedGetter () { if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { return } if ("development" !== 'production' && !(val in this.$store.getters)) { console.error(("[vuex] unknown getter: " + val)); return } return this.$store.getters[val] }; // mark vuex getter for devtools res[key].vuex = true; }); return res });
和mapState的实现几乎彻底同样,惟一的差异只有1点:就是最后不会出现value为函数的状况。直接拿的是对应模块上的getters:
return this.$store.getters[val]
与mapMutations能够映射模块的mutation为methods相似,mapActions也能够将模块的actions映射为methods,咱们来看看官方文档的介绍:
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` 'increment', // `mapActions` 也支持载荷: // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` 'incrementBy' ]), ...mapActions({ // 将 `this.add()` 映射为 `this.$store.dispatch('increment')` add: 'increment' }) } }
一样咱们来看看它是如何实现的,它的实现定义在VUEX源码中的866 ~ 890 行:
var mapActions = normalizeNamespace(function (namespace, actions) { var res = {}; normalizeMap(actions).forEach(function (ref) { var key = ref.key; var val = ref.val; res[key] = function mappedAction () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var dispatch = this.$store.dispatch; if (namespace) { var 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 });
和mapMutations的实现几乎彻底同样,惟一的差异只有1点:
咱们来具体分析一下代码的执行:
首先是拷贝载荷:
var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ];
而后是拿dispatch,若是mapActions函数提供了命名空间参数(即模块名),则会拿带命名空间模块的dispatch:
var dispatch = this.$store.dispatch; if (namespace) { var module = getModuleByNamespace(this.$store, 'mapActions', namespace); if (!module) { return } dispatch = module.context.dispatch; }
最后则会看对应action的value是否是函数:
也就是说,官方文档例子并不完整,它并无体现第二种状况,实际上,官方文档例子的完整形式还应当包括:
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions ('moduleName', { addAlias: function(dispatch, playload) { //将 `this.addAlias()` 映射为 `this.$store.dispatch('increment', amount)` dispatch('increment') //将 `this.addAlias(playload)` 映射为 `this.$store.dispatch('increment', playload)` dispatch('increment', playload) } }) } }
一样,mapActions上述映射方式都支持传递一个模块名做为命名空间参数,这个在官方文档也没有体现:
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions('moduleName', [ // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` 'increment', // `mapActions` 也支持载荷: // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` 'incrementBy' ]), ...mapActions('moduleName', { // 将 `this.add()` 映射为 `this.$store.dispatch('increment')` add: 'increment' }), ...mapActions('moduleName', { addAlias: function (dispatch) { // 将 `this.addAlias()` 映射为 `this.$store.dispatch('increment')` dispatch('increment') } }) } }
咱们能够举个例子证实一下:
const moduleA = { namespaced: true, state: { source: 'moduleA' }, mutations: { increment (state, playload) { // 这里的 `state` 对象是模块的局部状态 state.source += playload } }, actions: { increment (context) { context.commit('increment', 'testdata') } } } const store = new Vuex.Store({ state() { return { source: 'root' } }, mutations: { increment (state, playload) { state.source += playload } }, actions: { increment (context) { context.commit('increment', 'testdata') } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, mounted() { console.log(this.source) this.localeincrement() console.log(this.source) }, computed: Vuex.mapState([ 'source' ] ), methods: { ...Vuex.mapActions( { localeincrement (dispatch) { dispatch('increment') } }) } })
输出结果:
root roottestdata
另一个例子:
const moduleA = { namespaced: true, state: { source: 'moduleA' }, mutations: { increment (state, playload) { // 这里的 `state` 对象是模块的局部状态 state.source += playload } }, actions: { increment (context) { context.commit('increment', 'testdata') } } } const store = new Vuex.Store({ state() { return { source: 'root' } }, mutations: { increment (state, playload) { state.source += playload } }, actions: { increment (context) { context.commit('increment', 'testdata') } }, modules: { a: moduleA } }) var vm = new Vue({ el: '#example', store, mounted() { console.log(this.source) this.localeincrement() console.log(this.source) }, computed: Vuex.mapState('a', [ 'source' ] ), methods: { ...Vuex.mapActions('a', { localeincrement (dispatch) { dispatch('increment') } }) } })
输出结果:
moduleA moduleAtestdata
createNamespacedHelpers主要是根据传递的命名空间产生对应模块的局部化mapState、mapGetters、mapMutations、mapActions映射函数,它定义在VUEX源码的892 ~ 897行:
var createNamespacedHelpers = function (namespace) { return ({ mapState: mapState.bind(null, namespace), mapGetters: mapGetters.bind(null, namespace), mapMutations: mapMutations.bind(null, namespace), mapActions: mapActions.bind(null, namespace) }); };
isObject定义在VUEX源码的94 ~ 96 行,主要判断目标是不是有效对象,其实现比较简单:
//判断是否是object function isObject (obj) { return obj !== null && typeof obj === 'object' }
isPromise定义在VUEX源码的98 ~ 100 行,主要判断目标是不是promise,其实现比较简单:
function isPromise (val) { return val && typeof val.then === 'function' }
assert定义在VUEX源码的102 ~ 104 行,主要用来断言,其实现比较简单:
function assert (condition, msg) { if (!condition) { throw new Error(("[vuex] " + msg)) } }
到此这本VUEX学习笔记算是写完了,整体而言是对我的在学习VUEX源码过程当中的理解、想法进行的记录和总结,这其中除了不可避免的主观视角外,天然还会存在一些理解上的误差甚至错误,但愿看到这本书的人可以指正。