手写 Vuex(二)

书接上文:手写 Vuex (一) 中,实现了 Vuex 的基础功能,现在继续对其进行完善,实现模块化的状态管理。模块化可以帮助我们更好地组织和管理复杂的应用状态,使得状态的结构更加清晰和可维护。

格式化参数

将参数模块格式化为模块嵌套的树形结构(如下),方便我们后续的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//根模块
this.root = { // 模块的配置:包含当前模块的 state、getters、mutations、actions
_raw: xxx,
_children: { // 子模块
a模块: {
_raw: xxx,
_children: {},
state: xxx.state
},
b模块: {
_raw: xxx,
_children: {},
state: xxx.state
}
},
state: xxx.state
}

Module 类

创建 Module 类,通过 new Module 便可以得到格式化后的树形数据结构。

1
2
3
4
5
6
7
8
// module.js
export default class Module {
constructor(rootModule) {
this._raw = rootModule,
this._children = {}
this.state = rootModule.state
}
}

ModuleCollection 类

在这个类中,实现将用户传入的参数转化为格式化后的结果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// module-collection.js
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
constructor(options) {
// 注册模块 []表示路径 递归注册模块
this.register([], options)
}
register(path, rootModule) {
const newModlue = new Module(rootModule)

if (path.length == 0) { // 根模块
this.root = newModlue
} else {
const parent = path.slice(0, -1).reduce((pre, next) => {
return pre.getChild(next)
}, this.root)
parent.addChild(path[path.length - 1], newModlue)
}
// 注册子模块
if (rootModule.modules) {
forEachValue(rootModule.modules, (moduleValue, moduleName) => {
// 递归
this.register([...path, moduleName], moduleValue)
})
}
}
}

forEachValue

其中 forEachValue 方法提取为工具方法,方便后续复用

1
2
3
4
5
6
const forEachValue = (obj = {}, fn) => {
Object.keys(obj).forEach(key => {
fn(obj[key], key)
})
}
export default forEachValue

getChild、addChild

增加获取子模块和追加子模块方法,便于调用

1
2
3
4
5
6
7
8
9
10
11
12
// module.js
export default class Module {
...
// 获取子模块
getChild(key) {
return this._children[key]
}
// 追加子模块
addChild(key, module) {
this._children[key] = module
}
}

至此完成模块格式化为模块嵌套的树形结构,接下来重构 Store,实现 state、getter、commit、dispatch等

installModule

installModlue方法:将创建的树形结构上的状态、方法安装到 Store 实例上,就可以通过$store方式获取到对应的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
// 收集所有模块的状态
if (path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0, -1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
Vue.set(parent, path[path.length - 1], module.state)
}
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
// 注册所有模块到Store实例上
// this当前实例、根状态、路径、根模块
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
}
}

forEachChild

遍历安装当前模块的子模块

1
2
3
4
5
6
7
8
// module.js
export default class Module {
...
// 遍历当前模块的child
forEachChild(fn) {
forEachValue(this._children, fn)
}
}

resetStoreVm

实现 state 数据响应式响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function resetStoreVm(store, state) {
const wrappedGetters = store._wrappedGetters
const computed = {}
store.getters = Object.create(null)
// 通过使用vue的computed实现缓存
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = () => {
return fn()
}
// 代理
Object.defineProperty(store.getters, key, {
get: () => { return store._vm[key] }
})
})
// 将状态实现响应式
store._vm = new Vue({
data() {
return {
$$state: state
}
},
computed
})
}

class Store {
constructor(options) {
...
let state = this._modules.root.state
...
//实现state响应式
resetStoreVm(this, state)
}

get state() {
return this._vm._data.$$state
}
...
}

forEachGetters

扩展 Module 类,遍历 getters

1
2
3
4
5
6
7
8
9
10
11
// module.js
export default class Module {
...
// 遍历当前模块的getters
forEachGetters(fn) {
if (this._raw.getters) {
forEachValue(this._raw.getters, fn)
}
}

}

getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function installModule(store, rootState, path, module) {
...
module.forEachGetters((getters, key) => {
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(module.state)
}
})
...
}

class Store {
constructor(options) {
...
this._wrappedGetters = Object.create(null) // 存放所有模块的getters
...
}
}

commit(mutations)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function installModule(store, rootState, path, module) {
...
module.forEachMutations((mutations, type) => {
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[type] = (store._mutations[type] || [])
store._mutations[type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
mutations.call(store, module.state, payload)
})
})
...
}

class Store {
constructor(options) {
...
this._mutations = Object.create(null) // 存放所有模块的mutation
...
}
commit = (type, payload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type].forEach(fn => fn(payload))
}
}

forEachMutations

1
2
3
4
5
6
7
8
9
10
11
// module.js
export default class Module {
...
// 遍历当前模块的mutations
forEachMutations(fn) {
if (this._raw.mutations) {
forEachValue(this._raw.mutations, fn)
}
}

}

dispatch(actions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function installModule(store, rootState, path, module) {
...
module.forEachActions((actions, type) => {
store._actions[type] = (store._actions[type] || [])
store._actions[type].push((payload) => {
actions.call(store, store, payload)
})
})
...
}

class Store {
constructor(options) {
...
this._actions = Object.create(null) // 存放所有模块的actions
...
}

// dispatch
dispatch = (type, payload) => {
this._actions[type].forEach(fn => fn(payload))
}
}

forEachActions

1
2
3
4
5
6
7
8
9
10
11
// module.js
export default class Module {
...
// 遍历当前模块的actions
forEachActions(fn) {
if (this._raw.actions) {
forEachValue(this._raw.actions, fn)
}
}

}

namespace

默认情况下(或 namespace: false),模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果想要模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

getNamespaced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// module-collection.js
export default class ModlueCollection {
...
// 获取命名空间
getNamespaced(path) {
let root = this.root
return path.reduce((pre, next)=>{
// 获取子模块 查看是否有namespaced属性
root = root.getChild(next)
// 拼接上有namespace属性的路径
return pre + (root.namespaced ? next + '/' :'')
}, '')
}
}
1
2
3
4
5
6
7
// module.js
export default class Module {
...
get namespaced () {
return this._raw.namespaced
}
}

添加 namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function installModule(store, rootState, path, module) {
// 获取命名空间
const namespaced = store._modules.getNamespaced(path)

module.forEachMutations((mutations, type) => {
// 添加 namespace
store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
store._mutations[namespaced + type].push((payload) => {
mutations.call(store, module.state, payload)
})
})
module.forEachActions((actions, type) => {
// 添加 namespace
store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
store._actions[namespaced + type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key) => {
store._wrappedGetters[namespaced + key] = () => {
return getters(module.state)
}
})
...
}

严格模式

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

接收 strict 配置项,添加 mutation 提交状态标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Store {
constructor(options) {
...
// strict mode
const { strict = false } = options
this.strict = strict
// 添加 committing 状态
this._committing = false
//实现状态响应式
resetStoreVm(this, state)
}
// 在 mutation 之前,设置 _committing = true, 调用 mutation 之后更改状态
// 如此当状态变化时,_committing 为 true,说明是同步更改,false 说明是非 mutation 提交
_withCommit = (fn) => {
var committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
// 修改 commit 方法,使用 _withCommit 调用 mutation
commit = (type, payload) => {
if (!this._mutations[type]) {
throw new Error(`Mutation "${type}" not found`);
}
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach((handler) => {
handler(payload)
})
})
}
}
function resetStoreVm(store, state) {
...
// 如果开启了严格模式,则调用 enableStrictMode
if (store.strict) {
enableStrictMode(store)
}
}

enableStrictMode

监听 state 数据变化,判断 _committing 如果是 true 表示是同步执行,如果为 false,则会抛出错误提示

1
2
3
4
5
function enableStrictMode(store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
console.assert(store._committing, `[vuex] do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}

以上只是基于vuex源码实现了最核心的功能,但它帮助我们更好地理解了 Vuex 的模块化的实现原理。本来计划在这篇中实现 map辅助函数,但发现模块化的写起来实在太多了,所以将 mapState、 mapGetters、 mapMutations、 mapActions 放到下篇讲解。希望这篇文章对你有所帮助!更多详细信息请参考 vuex 源码

完整代码

系列文章