前言
为什么要使用模块?
由于使用单一的 state tree
, state
会变成一个臃肿的对象,我们可以使用模块,每一个模块都可以包含属于自己的 state / mutations / getters / actions
,甚至可以嵌套,形如
js
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> `moduleA`'s state
store.state.b // -> `moduleB`'s state
可以根据不同功能编写不同的模块,这样做不仅可以做到代码功能清晰,也可以避免写出长代码
格式化
虽然使用起来很方便,但是这个实现起来可一点不方便
对于用户传递的嵌套格式,需要进行格式化我们可以更方便处理的数据结构
对于嵌套结构什么数据结构是方便处理的呢?
对于可以无限嵌套的结构,使用 树
这个数据结构是最好不过了
ok,那我们就构建出一棵树
假如我们的传递的是这样的数据[点击查看👋]
js
state: {
namespaced: true,
name: "zs",
},
modules: {
aCount: {
namespaced: true,
state: {
name: "zs2",
},
mutations: {
add2(state) {
console.log("aaa")
state.name = 'lisi'
}
},
modules: {
bCount: {
namespaced: true,
state: {
name: "zs3",
},
mutations: {
add(state) {
console.log("bbb")
state.name = 'wangwu'
}
}
}
}
}
}
使用 ModuleCollection
类来处理,因为类方便扩展
js
new ModuleCollection(options)
我们需要构建出形如
js
{
state: { name: 'zs', aCount: {...}},
_children: {aCount: Module},
_rawModule: {state: {...}, modules: {...}}
}
- state 是方便获取模块中的 state
- _children 是为了 构建出一个子父查找链, 方便找到子模块
- _rawModule 保存原始数据
ok,我们来看看 ModuleCollection
其中的核心难点就在于查找,我们使用一个简单的🌰来梳理一下执行顺序
modules 中有嵌套了 aModule
和 bModule
, 其中在 aModule
中又嵌套了 a1Module
-
path 为空数组,newModule 为根模块
发现 newModule 中还有模块,遍历所有内部模块,key值是对象名,即
aModule
和bModule
, 然后递归调用 -
现在 path 为
[aModule]
现在 path 不为
[]
,进入到else
分支, 使用slice
把最后一个去掉,因为最后一个是自己, 执行get
方法找到所有父级 ,只要最近的父级把自己给添加进去 -
发现
aModule
下面还有模块,那么 path 变为[aModule,a1Module]
,然后执行递归 -
a1Module
下没有新的模块,那么就轮到到bModule
执行了
经过这样一来,我们的数据结构就变成了
上述源代码 [点击查看👋]
js
function forEachValue(obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
class Module {
constructor(rawModule) {
this._children = Object.create(null)
this._rawModule = rawModule
}
addChild(key, module) {
this._children[key] = module
}
getChild(key) {
return this._children[key]
}
}
class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule)
}
register(path, rawModule) {
const newModule = new Module(rawModule)
// 说明是根目录
if (path.length === 0) {
this.root = newModule
} else {
// 找到父级
// [ 'aModule' ] [ 'aModule', 'a1Module' ] [ 'bModule' ]
const parent = this.get(path.slice(0, -1))
console.log(" parent:", parent);
// 父级添加子模块
parent.addChild(path[path.length - 1], newModule)
}
// 说明有子 modules,需要递归注册
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 递归注册
this.register(path.concat(key), rawChildModule)
})
}
}
//找到所有的
get(path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
}
let x = new ModuleCollection({
modules: {
aModule: {
modules: {
a1Module: {
}
}
},
bModule: {}
}
})
到这一步,格式化数据已经完成了,剩下的就是对 state / commit
等api 的操作了
state
操作 state
和 格式化数据结构是一样的思路
js
const state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
以模块名为 key,value 为模块中的 state 作为对象挂载到 根state 上
🚝 这样你就完成了 store.state.aModule.state
重要的一步了(还需要其他方法配合)
上述源代码,可以直接复制执行 [点击查看👋]
js
function getNestedState(state, path) {
return path.reduce((state, key) => state[key], state)
}
function installModule( rootState, path, module) {
const isRoot = !path.length
if (!isRoot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
parentState[moduleName] = module.state
}
if (module._children) {
Object.entries(module._children).forEach(([key, child]) => {
installModule(rootState, path.concat(key), child)
})
}
}
let module = {
_children: {
aModule: {
state: { name: "aModule" },
_children: {
a1Module: {
state: { name: "a1Module" },
}
}
},
bModule: {
state: {
name: "bModule"
}
}
}
}
let rootState = { name: "zs" }
installModule(rootState, [], module);
console.log(rootState)
commit
同样的,commit 也是同样的道理
如果想要执行 aModule模块下面的 a1Module 下的 add 方法 , 需要这样执行 commit('aModule/a1Module/add')
,模块之间使用了 /
分割
js
function installModule(store, rootState, path, module){
// .....
// 获取 模块上的 namespace
const namespace = store._modules.getNamespace(path)
// 设置上下文
const local = makeLocalContext(store, namespace,path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
由于在 installModule
方法中使用了递归,我们获取了 父级路径 数组,通过 /
不断拼接 path 即可
如果模块上有 namespaced
属性并且为 true,我们就把对应的 key 值拼接上
js
getNamespace(path) {
// 根模块
let module = this.root;
return path.reduce((namespace, key) => {
// 获取对应的孩子
module = module.getChild(key);
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
makeLocalContext
设置上下文,父级 commit 中 可以访问子 state,不是重点
js
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
function makeLocalContext(store, namespace, path) {
const noNamespace = namespace === '';
const local = {
commit: noNamespace ? store.commit : (type, payload) => {
type = namespace + type;
store.commit(type, payload)
}
};
Object.defineProperties(local, {
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
registerMutation
这个方法就是把commit Name 注册进 _mutations
对象,等到 真正commit触发事件时执行,使用数组的原因是有可能 父级没有设置 namespace ,并且父级中的方法和自己方法重名,那么触发的时候就要一起触发
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)
})
}
结束
这个嵌套结构最重要的思想就是格式化,格式化出容易使用的数据结构
那个注册模块的方法很巧妙,通过路径 -1 找到父级然后注入进去
今天是周四4点41,快下班了,就这样吧