相关的util工具方法
- isObject方法,判断obj参数是否是一个对象。
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
复制代码
installModule 参数介绍
- store:指store对象实例
- rootState:根模块的state
- path:按序(模块嵌套层级)存储模块名的数组。上一章,注册模块有它的介绍。
- module:模块对象
- hot:布尔值,同内部变量isRoot一起,用于控制是否使用Vue.set设置state
function installModule(store, rootState, path, module, hot) {
// ...
}
复制代码
installModule 函数逐步分析
定义变量 isRoot 和 namespace
- installModule一旦调用,就会先在函数顶部定义两个变量isRoot和namespace。
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
复制代码
-
isRoot是一个布尔值。若是根模块(path.length = 0),则值为true。若是非根模块(path.length > 0),则值为false。
-
namespace是一个字符串。通过调用模块的getNamespace方法返回的,由模块名和’/’拼接而成的字符串。例如:’moduleB/’ 或 ‘moduleB/moduleA/’。关于这个方法的讲解,在上一章中已配合案例做出介绍。
在名称空间映射中注册模块
_modulesNamespaceMap 是初始化store实例时定义的一个对象,可理解为——模块名称空间映射表。它专门用来存储带命名空间的模块。
// module.namespaced 为true,则表明此模块开启了命名空间。
// 就是在定义模块时,设置了 namespaced: true的模块。
if (module.namespaced) {
// __DEV__ 开发环境下为true
// namespace是否已存在于 _modulesNamespaceMap 中
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
复制代码
下图展示的是存储完成后的 _modulesNamespaceMap 对象。
仔细阅读过vuex文档的同学们,应该会很容易理解_modulesNamespaceMap的作用。它同 mapState
,mapGetters
, mapActions
和mapMutations
这些函数有所关联。这些函数的源码都会用到这个对象。当使用这些函数来绑定带命名空间的模块时,为写起来方便,可将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。下面的示例引用自vuex官网文档。
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
复制代码
设置 state 对象
这个state指的是根模块的state(this._modules.root.state)。
isRoot 和 hot的值必须同时为false(取反为true),才能设置state。
if (!isRoot && !hot) {
// 获取父模块的 state。rootState是根模块的 state,path.slice(0, -1)返回一个
// 数组,这个数组不包含 path 数组的最后一位元素。
const parentState = getNestedState(rootState, path.slice(0, -1))
// 子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
// 给父模块的state对象添加新属性,这个属性是其子模块名,值是子模块的 state 对象
Vue.set(parentState, moduleName, module.state)
})
}
复制代码
可以看到,在使用Vue.set给父模块的state对象添加新属性时,是在store实例的 _withCommit 方法中调用的。
_withCommit(fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
复制代码
这个函数很简单。它先是保存_committing(布尔值,初始化时定义,默认false)的原始值,接着将其设置为true,然后执行fn函数(调用_withCommit时传入的函数,一般都是修改state),当函数执行完毕后,再将_committing设置为原来的值。
可是,这么做有何意义呢?看下面这段代码。
function enableStrictMode(store) {
store._vm.$watch(function () {
return this._data.$$state
}, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, {
deep: true,
sync: true
})
}
复制代码
只要开启严格模式,enableStrictMode方法就会被调用。这个方法用Vue中的$watch对this._data.$$state(它指向rootState)进行监听,一旦修改了rootState中的状态且store._committing的值又为false时,它就会抛错。初始化时,默认_committing为false。
但,在源代码中,修改状态是不可避免的。因此,为了防止报错,就把对修改状态的操作,放入_withCommit中执行,因为它会在修改状态前,把_committing设置为true,等修改完成后,再把_committing还原。so,明白否!
makeLocalContext 创建模块上下文环境
const local = module.context = makeLocalContext(store, namespace, path)
复制代码
makeLocalContext会为每个module生成对应的上下文环境。
其中定义了dispatch、commit、getters和state属性。下面是makeLocalContext函数的源码。
function makeLocalContext(store, namespace, path) {
// 判断命名空间路径是否存在
const noNamespace = namespace === ''
// 在local对象中定义dispatch、commit、getters和state,并返回local
const local = {
// 根据 namespace 判断。若是true,就直接调用store.dispatch,否则就先
// 对参数进行处理,然后才调用store.dispatch
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
// unifyObjectStyle 根据_type参数类型对接受的三个参数进行调整(下面会细说)
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options} = args
let { type } = args
// options.root为true时,将允许在命名空间模块里分发根的 action,否则只分发
// 当前模块内的action
// options不存在(null或undefined)或 options.root为false,则将命名空间路径
// 与 type(action函数名)进行拼接。拼接后的字符串:'moduleB/increment',作用
// 就是可以这样调用:this.$store.dispatch('moduleB/increment')
if (!options || !options.root) {
type = namespace + type
if (__DEV__ && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
// 同dispatch处理方式一样
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
type = namespace + type
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// 给local对象添加getters和state属性
// getter和state对象必须惰性获取,因为它们将被vm更新更改
Object.defineProperties(local, {
getters: {
get: noNamespace ?
() => store.getters : () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
复制代码
- unifyObjectStyle 方法
在解读这个方法之前,我们需要了解以下两点(均摘自官网):
- Actions 支持载荷方式和对象方式进行分发
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
复制代码
- dispatch 使用介绍
分发 action。options 里可以有 root: true,它允许在命名空间模块里分发根的 action。
返回一个解析所有被触发的 action 处理器的 Promise。
dispatch(type: string, payload?: any, options?: Object): Promise<any>
dispatch(action: Object, options?: Object): Promise<any>
复制代码
看出来了吗?从上面列出的两点内容,我们可以清楚的知道,action支持两种形式进行分发,且分发时传入的第一个参数类型并不一致。它可以是 String 或 Object。
为了处理这种情况,unifyObjectStyle方法就应运而生了。看下面代码。
function unifyObjectStyle(type, payload, options) {
// type是对象且type.type为真,则是以对象形式分发,需要要对参数进行调整,否则是载荷形式
// 分发。且对参数的调整是以载荷形式为准。
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
// __DEV__ 开发环境下为true
if (__DEV__) {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
// 将参数放在一个对象中返回。
return {
type,
payload,
options
}
}
复制代码
- makeLocalGetters 方法
// 缓存getters
function makeLocalGetters(store, namespace) {
// 是否存在缓存
if (!store._makeLocalGettersCache[namespace]) {
const gettersProxy = {} // 可理解为getters的代理对象
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 示例(仅为举例):type ——> 'moduleB/increment'
// 如果目标getter的名称空间与namespace不匹配,则跳过。
// 注意:此段代码和registerGetter方法结合看会更容易理解store.getters
// 中的属性即type,为什么会是:'moduleB/increment'(仅为举例),这种形式。
if (type.slice(0, splitPos) !== namespace) return
// 提取getter函数名
const localType = type.slice(splitPos)
// 为gettersProxy对象定义属性
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true // 可枚举
})
})
store._makeLocalGettersCache[namespace] = gettersProxy
}
return store._makeLocalGettersCache[namespace]
}
复制代码
- getNestedState 方法
获取模块相应的state,此方法的核心在于对数组方法reduce的使用。
function getNestedState(state, path) {
return path.reduce((state, key) => state[key], state)
}
复制代码
注册mutations、actions 和 getters
关于注册module的mutations、actions和getters这一部分的源码并不复杂,这里不在不在一一介绍,所以同学们要自己尝试调试一下哟。
// 注册module中的mutations、actions和getters,并将其this绑定到当前store对象
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
复制代码
注册子模块
使用模块中定义的forEachChild方法,遍历子模块进行注册。
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
复制代码
结束语
虽然没有详尽的去解释代码,但不妨碍这文章还是有点东西的。啊哈哈。。。