4. install 注册
Store
类的所有实现都了解完了,再来看一下入口文件里还有什么,突然发现忘记看一下非常重要的 install
方法了,根据 install
方法的导入路径找到相应的函数:
// 提供install方法 export function install (_Vue) { if (Vue && _Vue === Vue) { if (__DEV__) { console.error( '[vuex] already installed. Vue.use(Vuex) should be called only once.' ) } return } Vue = _Vue applyMixin(Vue) }
当我们调用 Vue.use(vuex)
时,调用这个方法,先判断 vuex
是否已被注册,若已被注册,则不执行任何操作 ; 若没有被注册,则调用 applyMixin
方法,现在移步到 ./mixin.js
文件:
export default function (Vue) { const version = Number(Vue.version.split('.')[0]) // 2.x版本直接通过全局混入Vue.mixin的方式挂载store if (version >= 2) { Vue.mixin({ beforeCreate: vuexInit }) } else { // 兼容1.x版本 const _init = Vue.prototype._init Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit _init.call(this, options) } } // 将vuex混入到$options中 function vuexInit () { // 获取当前组件的 $options const options = this.$options // 若当前组件的$options上已存在store,则将$options.store赋值给this.$store(一般是用于根组件的) if (options.store) { this.$store = typeof options.store === 'function' ? options.store() : options.store } // 当前组件的$options上没有store,则获取父组件上的$store,即$options.parent.$store,并将其赋值给this.$store(一般用于子组件) else if (options.parent && options.parent.$store) { this.$store = options.parent.$store } } }
applyMixin
方法先判断了 Vue
的版本号,主要做的是一个向下兼容 Vue 1.x
的版本,这里我对 Vue 1.x
的版本不太熟悉,所以就直接看 Vue 2.x
版本的处理方式吧
通过 Vue.minxin
方法做了一个全局的混入,在每个组件 beforeCreate
生命周期时会调用 vuexInit
方法,该方法处理得非常巧妙,首先获取当前组件的 $options
,判断当前组件的 $options
上是否有 sotre
,若有则将 store
赋值给当前组件,即 this.$store
,这个一般是判断根组件的,因为只有在初始化 Vue
实例的时候我们才手动传入了 store
; 若 $options
上没有 store
,则代表当前不是根组件,所以我们就去父组件上获取,并赋值给当前组件,即当前组件也可以通过 this.$store
访问到 store
实例了
这里不得不感叹,这个处理方式太棒了。
5. 辅助函数
store
实例生成并且也 install
到 Vue
上了,看一下入口文件中只剩下辅助函数了,它们有 mapState
、mapGetters
、mapMutations
、mapActions
、createNamespacedHelpers
,进到相应的文件 ./helpers.js
中看一下
import { isObject } from './util.js' 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 () { 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 }) export const mapMutations = normalizeNamespace((namespace, mutations) => { const res = {} if (__DEV__ && !isValidMap(mutations)) { console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object') } 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 }) export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} if (__DEV__ && !isValidMap(getters)) { console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object') } 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 } if (__DEV__ && !(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 }) export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} if (__DEV__ && !isValidMap(actions)) { console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object') } 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 }) /** * 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) }) 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] })) } function isValidMap (map) { return Array.isArray(map) || isObject(map) } 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) } } 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 }
整个文件里东西非常多,但我们很明确地知道,我们主要看的就是那几个辅助函数,观察发现,每个辅助函数都会先调用 normalizeNamespace
函数进行处理,那么我们就先看看这个函数做了什么:
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) } }
根据函数名的字面意思知道这应该是根据不同的调用方法,标准化命名空间的。
首先返回一个函数,接收两个参数,即 namespace
和 map
,这也是我们调用辅助函数时可以传入的两个参数 ;
然后判断 namespace
是否为字符串形式,若不是字符串,则表示是普通的调用方式,例如:
mapMutations(['first/second/foo', 'first/second/bar']) mapMutations({ foo: 'first/second/foo', bar: 'first/second/bar', })
这种情况,就直接将第一个参数 namespace
赋值给映射变量 map
,而 namespace
设为空
若是字符串的话,则表示调用的是带命名空间的绑定函数的,例如:
mapState('first/second', ['foo', 'bar']) mapState('first/second', { foo: 'foo', bar: 'bar', })
处理好这两种不同的调用方式以后,调用一下 fn
,并将 namespace
和 map
作为参数
那么就先从 mapState
开始看吧
5.1 mapState
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 () { 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 })
这里的 namespace
是一个字符串,states
是我们刚才处理好的映射变量 map
首先创建一个空对象 res
,这是我们最后处理好要返回的变量 ;
然后通过 isValidMap
方法判断 map
是否符合要求,即是否是数组或对象 ;
再然后调用了 normalizeMap
方法处理了变量 states
,从字面意义上来看,这是用来标准化该变量的,因为毕竟有可能是数组又有可能是对象嘛,所以要统一一下。
来看一下 normalizeMap
方法的实现:
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] })) }
首先仍然要先判断 map
是否合法,若不合法,则返回空数组,避免后续的代码报错 ;
然后判断 map
是否为数组,若是数组,则遍历 map
进行处理:
将 [1, 2, 3] 变成 [{key: 1, val: 1}, {key: 2, val: 2}, {key: 3, val: 3}]
若 map
不是数组,则一定为对象,那么同样也要把其处理成跟上面一样的格式:
将 {a: 1, b: 2, c: 3} 变成 [{key: a, val: 1}, {key: b, val: 2}, {key: c, val: 3}]
处理好了以后就直接返回,在得到标准化以后的 map
后要对其进行 forEach
遍历,将遍历到的每一个对象经过处理后存放在 res
中,即 res[key] = function mappedState() {...}
,来看一下这个 mappedState
里做了什么处理
首先获取一下根模块上的 state
和 getters
// 获取根模块的 state 、getters let state = this.$store.state let getters = this.$store.getters
然后判断是否存在命名空间,即 namespace
是否为空,若为空,则不做任何处理 ; 否则调用 getModuleByNamespace
方法获取到 namespace
对应的模块 module
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 }
可以看到 store._modulesNamespaceMap
终于派上了用场,在生成 Store
实例注册所有模块的时候,将带有命名空间的模块都存储在了该变量上,原来是在这里用上了
然后将刚才声明的变量 state
和 getters
替换成 module
对应上下文中的 state
和 getters
if (namespace) { // 获取命名空间namespace对应的模块 const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } // 将 state 、getters 变成该模块上下文中的 state 、getters state = module.context.state getters = module.context.getters }
这个 context
也是非常的巧妙,在注册模块的时候,获取到该模块的上下文的同时,还将其存储了一下,即:
const local = module.context = makeLocalContext(store, namespace, path)
之前看到的时候不知道有啥用,但在这里看到后,觉得真的非常得赞 👍
确定好了 state
和 getters
的值,最后就可以返回值了
return typeof val === 'function' ? val.call(this, state, getters) : state[val]
这里还做了一层处理是因为要处理两种不同的方式,例如:
mapState({ foo: state => state.foo, bar: 'bar' })
在这里我又发现了一个官方文档里没有提及的,就是以函数形式返回的时候,还能接收第二个参数 getters
,即:foo: (state, getters) => state.foo + getters.bar