六、Api解析(二)
在vue组件中,除了可以通过this.$store进行对应state,getters的获取,通过commit触发mutations,通过dispatch触发actions外,还暴露了几个辅助函数,帮助我们更好的操作vuex中的数据。
1.mapState
在调用的时候,我们可以通过往mapState函数中传递数组或对象获取相应的state。
参数为对象
computed: mapState({
count: state => state.count,
countAlias: 'count',
})
参数为数组
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
mapState函数定义在src文件夹下的helpers.js文件中,首先最外层包裹了normalizeNamespace函数,normalizeNamespace函数他会对拿到的参数进行解析,如果我们只传递了一个参数(数组或对象),那么在执行typeof namespace !== 'string'的时候会执行其中的逻辑map = namespace;namespace = '',这样在真正执行mapState内部的函数,参数传递就进行了统一处理。如果我们传递了两个参数,那么会判断我们传递的第一个参数namespace是否结尾是/,如果不是,会为我们在末尾拼接/,最终把处理过后的namespace和map两个参数传入内部的函数去执行。mapState内部的函数首先定义一个空的res变量,接着把第二个参数states作为参数,传入normalizeMap函数。normalizeMap函数的作用是把我们传入的数或者对象统一处理为一个数组对象,这个数组对象中key属性对应的value就是我们传入的key(如果是数组,直接把string作为key),然后把value作为数组对象的val。接着对这个数组对象进行遍历,把每一项的key作为res的key,value是定义的mappedState函数。最终把res对象return出去。也就是说最终当我们执行mapState辅助函数,那么最终拿到的是一个res,他的key是我们传入的key,value是mappedState函数,如果我们把他放入computed中,由于computed在访问时,会自动求值,会把我们的value,mappedState函数作为computed的getter执行。mappedState函数首先会通过this.$store.state和 this.$store.getters拿到全局的state和getters,然后会判断namespace是否为空,如果我们传入了这个字段(代表我们希望获取到modules中的state),那么他首先会把store实例,'mapState'字符串和传入的namespace作为参数调用getModuleByNamespace函数。getModuleByNamespace函数通过传入的namespace,去访问store._modulesNamespaceMap[namespace],store._modulesNamespaceMap在installModule函数中判断当前模块是否有namesapced,如果有,会把当前拿到的module注册在store._modulesNamespaceMap下。也就是说最终getModuleByNamespace会根据namespace拿到对应的module。拿到对应的module,mappedState函数接着会通过module.context拿到对应的state和getters。module.context是在installModule函数中,拿到makeLocalContext函数的结果进行注册的。其中的getter和state是经过处理后的,访问getter其实是最终store.getters[type]中的getter,state则为store._vm.state。拿到state和getters最后会判断我们传入的val,如果val是函数,则执行这个函数,并且把state和getters作为两个参数传入,否则返回state[val],所以当我们如果写的value是一个函数,那么第一个参数就是当前namespace对应的module的state。
// src/helpers.js
/**
* 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]
}
...
})
return res
})
...
/**
* 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] }))
}
...
/**
* 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 (__DEV__ && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
2.mapGetters
在调用的时候,我们可以通过往mapGetters函数中传递数组或对象获取相应的getters。参数为对象
computed: mapState('modulesA/', {
computedcountA: 'computedcount'
})
参数为数组
computed: mapState('modulesA/', ['computedcount'])
mapGetters同样定义在helper文件中,和mapState相同,他首先通过normalizeNamespace函数进行包裹(对传入的参数进行统一的处理,最终处理为namespace和map)。接着他也会定义一个res对象,用来获取存储的getters对象 ,并最终返回。通过normalizeMap函数对传入的map进行统一的处理成一个数组对象后,进行遍历。并把key作为res的key,value为mappedGetter函数。当我们访问getters的时候,会去执行mappedGetter函数,mappedGetter函数中的处理和mappedState函数有所不同,他首先会判断namespace,如果有的话会接着通过getModuleByNamespace函数判断是否可以通过传入的namespace找到对应的module,如果没有对应的module则会结束函数的执行,也是说,如果我们传入了namespace但是却没有找到对应的module,则会结束函数的执行。getModuleByNamespace函数会在dev模式下,如果没有找到对应的module,会报出这样的警告console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)当我们在开发的过程当中遇到了这样的报错,就说明我们传入的namespace可能是有问题的,vuex并没有通过这个namespace找到对应的modle。mappedGetter函数最终会返回this.$store.getters[val],store.getters在执行resetStoreVM函数的时候,在遍历store._wrappedGetters的时候,通过Object.defineProperty进行注册的,他的getter为() => store._vm[key],也就是store的vm实例的computed。store._wrappedGetters是在执行installModule中的registerGetter函数的时候,进行绑定的。
// src/helpers.js
/**
* Reduce the code which written in Vue.js for getting the getters
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} getters
* @return {Object}
*/
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
...
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
...
return this.$store.getters[val]
}
...
})
return res
})
3.mapMutations
在调用的时候,我们可以通过往mapMutations函数中传递数组或对象获取相应的mutations。
参数为对象
methods: {
...mapMutations('modulesA', {
add(commit, ...rest) {
commit('add', ...rest)
},
addCount:'add'
})
},
参数为数组
methods: {
...mapMutations('modulesA', ['add'])
},
在调用的时候,我们可以通调用mapMutations,生成组件的mutationn方法,第一个参数和mapState和mapGetters相同都是可选的传入namespace,第二个参数可以是一个数组或者对象,对象的值可以是一个string,也可以是一个function。mapMutations函数也是通过normalizeNamespace进行一层传入的参数的处理。处理之后首先也会定义一个对象res,然后通过normalizeMap函数对传入的map进行处理。之后遍历处理后的map,以mutations的key作为res的key,mappedMutation函数,作为value,最终返回res。这块的处理和之前的mapState和mapGetters是相同的。当我们调用对用的mutations的时候,会去执行mappedMutation函数,mappedMutation函数首先会通过=this.$store.commit拿到store实例的commit方法,然后会判断是否传入了namespoace,如果传入了namespoace,同样也会通过getModuleByNamespace函数拿到对应的module,然后重新定义当前的commit为module.context.commit,也就是说如果没有传入namespace(全局mutations)则会使用全局的commit方法,如果传入了namespace,则会去找到对应module的局部commit方法。module.context.commit在执行installModule函数中的makeLocalContext进行了定义,他会判断当前是否有namespace,如果有的话他会重新定义在执行store.commit的时候,传入的第一个参数type为拼接了namespace的结果。最后会判断拿到val是否是一个function,如果是一个function,则会去执行这个函数,并且把拿到的commit(局部或全局commit),和传入的剩余参数作为调用传入的这个函数的参数。也就是说如果我们写的是一个函数,那么函数的第一个参数将会是commit。如果判断不是一个函数,则会执行拿到的commit,并把传入的mutations名称作为第一个参数,传入的其他参数也传入。这样通过局部的commit再去找到完整的当前module的mutations的函数,最终调用。
// src/helpers.js
/**
* Reduce the code which written in Vue.js for committing the mutation
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept another params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
* @return {Object}
*/
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
if (namespace) {
const 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
})
4.mapActions
在调用的时候,我们可以通过往mapActions函数中传递数组或对象获取相应的actions。
参数为对象
methods: {
...mapActions('modulesA', {
addCount:'addCount',
async addCount(dispatch) {
await dispatch('addCount')
}
})
参数为数组
methods: {
...mapActions('modulesA', ['addCount'])
},
mapActions函数和mapMutations函数几乎是相同的,只是把commit换成了dispatch,这里就不在赘述他的实现步骤了。
5.createNamespacedHelpers
对于组件绑定的上述4个方法,的确帮助我们节省了部分的代码,但是如果还是面临一个问题,如果我们当前的组件需要操作同一个namespace下的state,action,mutation,getter,则需要传入4次namespace,所以createNamespacedHelpers函数正是为了解决这个问题而存在的。createNamespacedHelpers函数接受一分为参数namespace,然后向外暴露了一个对象,这个对象中有4个属性,分别代表mapState,mapGetters,mapMutations,mapActions, mapState: mapState.bind(null, namespace),他巧妙的利用了bind函数的特性,把传入的namespace作为第一个参数,之后当我们再去写参数的时候,就是第二个参数了,这样第一个参数namespace就被提前定义了。
// src/helpers.js
/**
* 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)
})
6.registerModule和unregisterModule
vuex还提供了动态对module的注册和注销
- 注册模块
this.$store.registerModule('c', {
namespaced: true,
...
})
this.$store.registerModule('c', {
namespaced: true,
...
})
registerModule函数首先对拿到的的第一个参数path做一个类型判断,如果他是string类型,则会执行path = [path]把他变成一个数组。接着会判断path是否是一个数组,如果不是一个数组,则报错这是因为registerModule接下来的第一步是通过this._modules.register,对module进行注册,而ModuleCollection实例,接受的path是一个数组类型的。接着会判断path.length > 0,如果我们第一个参数传入了一个空的字符串或者空数组,那么在执行register的时候,代表他是一个根部module,而registerModule函数不允许注册根部的module。判断了前置条件,接着会调用this._modules.register(path, rawModule),this._modules是ModuleCollection实例,其中的register方法是用来注册Module实例的。执行完这一步,module实例创建完毕,接着会通过installModule安装模块,最终调用resetStoreVM函数,对store._vm进行重新的注册(data,computed),最后对旧的store._vm通过$destroy进行销毁。
// src/store.js
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}
this._modules.register(path, rawModule)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)
}
- 注销模块
this.$store.unregisterModule('c')
this.$store.unregisterModule(['modulesA','c'])
unregisterModule函数和registerModule函数相同,也会首先判断传入的path的类型,如果是string,则会执行path = [path],接着判断如果不是数组则报错。接着调用 this._modules.unregister(path),ModuleCollection实例的unregister函数,会首先通过传入的paththis.get(path.slice(0, -1))拿到父module,然后通过path拿到传入的keyconst key = path[path.length - 1],拿到key和parent,则执行 const child = parent.getChild(key),module实例的getChild函数,实际是获取了当前module的_children属性,_children属性是在执行ModuleCollection实例的register函数的时候,通过module实例的addChild进行添加。如果通过key找到了对用的module实例,则会parent.removeChild(key),也就是module实例的delete this._children[key],通过delete删除对应的父子依赖关系。接着unregisterModule会通过this._withCommit去修改vm实例的data,首先通过getNestedState(this.state, path.slice(0, -1))找到对应的父state,然后调用Vue.delete(parentState, path[path.length - 1]),删除vm实例中对应的模块的state。删除之后执行resetStore,resetStore函数把store实例的_actions,_mutations,_wrappedGetters和_modulesNamespaceMap都重置为了空,重新通过store实例拿到state,通过调用installModule和resetStoreVM函数进行重新安装和重新注册vm实例。
// src/store.js
unregisterModule (path) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
this._modules.unregister(path)
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
...
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}