这是对vuex3版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章只讲解vuex中的Module,这也是vuex官网中“核心概念”的最后一个。
想了解vuex中的其他源码分析,欢迎参考我发布的下列文章:
VUEX 3 源码分析——1. 理解State
VUEX 3 源码分析——2. 理解Getter
VUEX 3 源码分析——3. 理解Mutations
VUEX3 源码分析——4. 理解Action
VUEX 3 源码分析——5. 理解Module
VUEX 3 源码分析——6. 理解命名空间namespace
VUEX 3 源码分析——7. 模块的动态注册和卸载
官方示例
- 对于下面的代码示例,可以发现模块内部的state、getter、mutation、action的调用方式和之前未使用module分块时有所不同。
- state的访问变成了store.state.moduleName
- mutation 和 getter,接收的第一个参数是模块的局部状态对象。
- action局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
},
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
module的初始化
- 假设我们按上面示例代码的moduleA和moduleB的内容和结构进行Store的实例化,看下vuex的源码会如何处理。
- 首先,遍历store初始化时定义的modules内容,调用register方法,这里传入的参数path = [‘moduleA’](path.concat(key) 逻辑),rawModule = 我们定义的moduleA的所有内容。
- 通过 this.get 方法获得父模块。
- 通过 父模块的 addChild 方法,将子模块赋值到父模块的_children中。
// store.js
export class Store {
constructor(options = {}) {
this._modules = new ModuleCollection(options)
}
...
}
// ./module/module-collection.js
export default calss ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
get(path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
register(path, rawModule, runtime=true) {
...
if (path.length == 0) {
this.root = rawModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length-1], newModule)
}
}
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// ./module/module.js
export default class Module {
constructor(rawModule, runtime) {
this._children = Object.create(null)
...
}
addChild(key, module) {
this._children[key] = module
}
getChild(key) {
return this._children[key]
}
}
installModule函数中对module的处理
- 在installModule函数中,遍历根模块的_children,递归调用installModule。
- 将子模块的state以子模块名为key,绑定到父模块的state中,这里就解释了为什么对子模块state是通过store.state.a或者store.state.b来访问的。
// store-util.js
// installModule(this, state, [], this._modules.root) in store.js
export function installModule(store, rootState, path, module, hot) {
const isRoot = !path.length
// 由于示例没有设置命名空间,所以所有的namespcae都为空字符串
const namespace = store._modules.getNamespace(path)
...
// 将子模块的state绑定到父模块的state中
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
...
parentState[moduleName] = module.state
})
}
// 生成局部的包含state, getters等属性的对象
const local = module.context = makeLocalContext(store, namespace, path)
...
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
- 在每个模块中,都生成了一个local对象,这个对象有state,getters,commit和dispatch四个属性,不考虑命名空间namespace的情况下,getters,commit和dispatch都是对应store的getters,commit和dispatch,而state对应子模块的state。
./store-util.js
function makeLocalContext(store, namespace, path) {
const noNamespace = namespace === '' // 这里的store实例没有使用命名空间,所以等于True
const local = {
dispatch: noNamespace ? store.dispatch : ...,
commit: noNamespace ? store.commit : ...
// 省略的内容是使用了命名空间时的逻辑
}
Object.defineProperties(local, {
getters: {
get: noNamespace ? () => store.getters : ...
},
state: {
get: () => getNestedState(store.state, path)
}
})
}
export function getNestedState(state, path) {
return path.reduce((state, key) => state[key], state)
}
- 由于local中的state是当前子模块的局部状态,所以模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。但是分析 registerGetter 函数可以发现,getter除了接受的局部state作为第一个参数以外,还接受局部的getters以及全局的state和getters。
- 而对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState。同时分析下面的 registerAction 函数可以发现,action和getter一样,也接受一些全局内容。
// ./store-util.js
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload) // local.state是子模块的state
})
}
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {return}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
...
return res
})
}
- 另外,由于未使用命名空间,即namespace为空字符串,假如局部模块定义的某个getter,mutation和action和全局定义的或者另一局部模块定义的名称相同,会发生一些问题。具体会发生什么问题,可以看这些内容各自的register函数源码就能明白,我在之前的文章中也有贴出,这里就不多解释了。
- 这就是官网中描述的“默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。”
export function installModule (store, rootState, path, module, hot) {
...
// namespace = ""
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)
})
}
为了避免模块与模块之间的一些命名问题,维持更高的封装度和复用性,尤其在项目复杂庞大的情况下,就需要使用官网说的 namespaced: true的方式使其成为带命名空间的模块。
以上就是官网上Module的模块的具体状态相关源码,下一遍会分析Module中,使用了命名空间后会和现在的逻辑有什么不同。