文章目录
module基本使用
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。
比如在根state下添加两个模块:a和b,其中a没有声明namspaced
modules: {
// 继承父模块的命名空间
a: {
state: {
name: 'a',
age: 20
},
getters: {
myAge: (state) => {
return state.age + 'a'
}
}
},
b: {
namespaced: true,
state: {
name: 'b',
age: 21
},
getters: {
myAge: (state) => {
return state.age + 'b'
}
}
}
}
然后页面调用:
<br>
<span>module:a的getter年龄:{{$store.getters['a/myAge']}}</span>
<br>
<span>module:b的getter年龄:{{$store.getters['b/myAge']}}</span>
a并不能展示出来,b可以正常展示。根sotre中定义了myAge
的getter,所以还会报一个重复定义的错误。
将 a中的myAge
改为aAge
,就可以正常使用了:
a: {
state: {
name: 'a',
age: 20
},
getters: {
aAge: (state) => {
return state.age + 'a'
}
}
},
<span>module:a的getter年龄:{{$store.getters.aAge}}</span>
关于namspaced 如果没定义的话,会继承父模块命名空间
假如在上面例子中,a模块里还嵌套了一个c模块,并且c模块声明了namspaced
:
a: {
state: {
name: 'a',
age: 20
},
getters: {
aAge: (state) => {
return state.age + 'a'
}
},
modules: {
c: {
namespaced: true,
state: {
name: 'c',
age: 201
},
getters: {
myAge: (state) => {
return state.age + 'c'
}
}
}
}
},
由于a没有声明namespaced,那么最终使用的时候,就不是通过[a/c/myAge]
,而是直接[c/myAge]
:
<span>module:c的getter年龄:{{$store.getters['c/myAge']}}</span>
模块收集
为了方便处理,针对用户在store中定义的各种模块,最后我们都会转换为树的格式:
{
module:'父模块',
state:xxx,
children:{
a:{
module:'a对应的模块',
state:xxx,
children:{
.....
}
}
}
}
新建一个文件夹module
,然后在该文件夹下新建文件module-collection.js
,用来将用户定义的store处理成上面的树的格式。
module-collection.js:
import { forEach } from '../util'
class ModuleCollection {
constructor (options) {
// 对数据进行格式化操作
this.root = null // 保存根
this.register([], options)
}
// patchStack : path 栈,用来确定父子关系
register (pathStack, rootModule) {
const newModule = {
_raw: rootModule,
_children: {},
state: rootModule.state
}
if (pathStack.length === 0) { // 说明初始化,是root
this.root = newModule
}else{ // 来到这里,存在父子关系了,比如遍历到 a的时候是 [a],遍历到c的时候是 [a,c],那么前一个就是父节点,通过 pathStack.slice(0,-1)过滤掉当前的节点
// 第一次进来时候只有[a],通过 slice(0,-1),为空,那 memo 就是赋的默认值 rootModule,然后 通过 rootModule._children[a] = newModule ,a 挂载到 根节点上
// a 完了之后 扫描子 module c, 此时 pathStack是[a,c], 通过 slice(0.-1)获取到的 a 即 c的父节点,然后 a._children[c] = newModule
// 后面依次类推.....
let parent = pathStack.slice(0,-1).reduce((memo,current)=>{
return memo._children[current]
},this.root)
parent._children[pathStack[pathStack.length - 1]] = newModule
}
if (rootModule.modules) { // 如果定义了modules,说明有children
forEach(rootModule.modules, (module, key) => {
this.register(pathStack.concat(key),module)
})
}
}
}
export default ModuleCollection
/*
最终要转换成:
this.root = {
_raw:用户定义的模块,
state:当前模块的state,
_children:{
// 孩子列表
a:{
_raw:a的模块,
state:a的state,
_children:{
// 孩子列表
}
}
}
}
**/
// 主要要通过栈来确定父子关系
这里主要是对数据进行格式化。
类的抽离
当前在ModuleCollection 的 register 中,创建 newModule 的功能和 register 其实可以看做两种功能,为了方便后续维护和扩展,将 newModule 的过程单独抽离成类:Module。
在module文件加下新建文件module.js
,
export default class Moudle {
constructor (rawMoudle) {
this._raw = rawMoudle
this._children = {}
this.state = rawMoudle.state
}
getChild (key) {
return this._children[key]
}
addChild (key, module) {
this._children[key] = module
}
}
在 ModuleCollection.js
中进行调用:
// 创建 newModule 的方式改为这样
const newModule = new Module(rootModule)
// 获取child 改为这样
const parent = pathStack.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current)
}, this.root)
// 添加child 的逻辑改为这样
parent.addChild(pathStack[pathStack.length - 1], newModule)
模块安装
经过上一步的操作,数据已经被格式化为我们想要的结构,接下来就是进行安装。在 store.js
中,将格式化后的结果赋值给 this._module
, 然后 需要将定义的所有的getters,actions,mutations 进行收集。
没有namspaced 的时候,getters是直接被放到根上,mutations,actions 会被合并成数组。
声明installModule
方法,参数分别是store
、path栈
、当前模块
。
遍历上一步的格式化树。因为格式化的是module
类中的,所以可以在Module
类中扩充 forEach
function installModule(store,path,module){
}
处理收集 mutations,actions,getters 等
module.js
module.js 中新增下面几个方法:
forEachMutations (cb) {
this._raw.mutations && forEach(this._raw.mutations, cb)
}
forEachActions (cb) {
this._raw.actions && forEach(this._raw.actions, cb)
}
forEachGetters (cb) {
this._raw.getters && forEach(this._raw.getters, cb)
}
forEachChildren (cb) {
this._children && forEach(this._children, cb)
}
然后在installModule
中调用:
// 遍历 安装 getters,mutations,actions 等
// 与 store 关联不大的方法,所以放在外面,没有作为成员方法
function installModule (store, path, module) {
module.forEachMutations((mutation, key) => {
store.mutations[key] = store.mutations[key] || []
store.mutations[key].push((payload) => {
mutation.call(store, module.state, payload)
})
})
module.forEachActions((action, key) => {
store.actions[key] = store.actions[key] || []
store.actions[key].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetters((getter, key) => {
store.wrapperGetters[key] = function () {
getter(module.state)
}
})
module.forEachChildren((child, key) => {
installModule(store, path.concat(key), child)
})
}
处理 state
接下来是处理 state, state 的处理需要按照父子关系进行嵌套,所以用到了 path 参数。我们最终在调用 state的时候,对于module 中的子模块是通过如下方式来调用:$store.state.a.name
,所以我们最终需要将state 处理成如下格式:
{name:'root',age:1,a:{name:'a',age:2}}
基于之前写的installModule
,多传一个 state 参数function installModule (store, state, path, module)
:
installModule:
// 基于path 进行state处理 {name:'root',age:1,a:{name:'a',age:2}} 最后调用的时候就是通过 store.state.a.name 来获取
if (path.length > 0) {
// 同样 还是基于reduce
// 第一次进来 parent 就是 rootState,
// 第二次进来 parent 是 a
// 每次都是从 rootState开始遍历查找父节点
const parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, state)
// 这样写新添加的state不会有响应式了
// parent[path[path.length - 1]] = module.state
Vue.set(parent, path[path.length - 1], module.state)
}
然后后续的逻辑基本就和最初的基本版实现一样,借助Vue实例实现响应式,完整如下:
import { Vue } from './install'
import ModuleCollection from './module/module-collection'
import { forEach } from './util'
// 遍历 安装 getters,mutations,actions 等
// 与 store 关联不大的方法,所以放在外面,没有作为成员方法
function installModule (store, state, path, module) {
// 基于path 进行state处理 {name:'root',age:1,a:{name:'a',age:2}} 最后调用的时候就是通过 store.state.a.name 来获取
if (path.length > 0) {
// 同样 还是基于reduce
// 第一次进来 parent 就是 rootState,
// 第二次进来 parent 是 a
// 每次都是从 rootState开始遍历查找父节点
const parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, state)
// 这样写新添加的state不会有响应式了
// parent[path[path.length - 1]] = module.state
Vue.set(parent, path[path.length - 1], module.state)
}
module.forEachMutations((mutation, key) => {
store.mutations[key] = store.mutations[key] || []
store.mutations[key].push((payload) => {
mutation.call(store, module.state, payload)
})
})
module.forEachActions((action, key) => {
store.actions[key] = store.actions[key] || []
store.actions[key].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetters((getter, key) => {
store.wrapperGetters[key] = function () {
getter(module.state)
}
})
module.forEachChildren((child, key) => {
installModule(store, state, path.concat(key), child)
})
}
class Store {
constructor (options) {
// 格式化后的
this._modules = new ModuleCollection(options)
console.log('result:', this._modules)
// 通过上一步格式化后的数据 接下来进行安装,将模块中所有的getters,mutations,actions 进行收集
// 没有 namespaced 的时候 getters都放在根上,actions,mutations会被合并成数组
this.mutations = {}
this.actions = {}
this.wrapperGetters = {}
this.getters = {}
const computed = {}
const state = options.state
installModule(this, state, [], this._modules.root)
forEach(this.wrapperGetters, (getter, key) => {
computed[key] = getter
Object.defineProperty(this.getters, key, {
get () {
return this._vm[key]
}
})
})
this._vm = new Vue({
data: {
$$state: state
},
computed
})
}
get state () {
return this._vm._data.$$state
}
// 箭头函数 绑定this
commit = (type, payload) => {
// 暂未考虑namespaced 所以都是数组
this.mutations[type] && this.mutations[type].forEach(fn => fn(payload))
}
dispatch = (type, payload) => {
this.actions[type] && this.actions[type].forEach(fn => fn(payload))
}
}
export default Store
实现命名空间
在vuex的使用过程中我们知道,如果添加了命名空间,那么在调用的时候 key 就是当前模块路径的一个拼接。比如 :
{
name:'root',
age:1,
modules:{
a:{
name:'a',
age:2,
modules:{
c:{
namespaced:true,
name:'c',
age:3,
getters:{
myAge:(state){
return state.age + 'c'
}
}
}
}
}
}
}
想要获取c的值,由于 a 没有 声明 namespaced,所以继承父级命名空间,所以需要通过 $store.state.c.name
来获取 name 值,通过$store.getters['c'].myAge
来获取getters的值。如果 a也声明了 namspaced, 那获取c的getters 就变为$store.getters['a/c'].myAge
。
所以 关键步骤就是在收集 mutations,getters,actions 等的时候(state我们在收集的过程中就已经是嵌套关系了),将 key 设置为 当前模块的路径的拼接。
首先在 Module
类中声明一个get 方法 namespaced
,用来判断当前模块是否声明了 namespaced :
export default class Module{
....之前逻辑
get namespaced(){
return !!this._raw.namespaced
}
}
在ModuleCollection
类中添加获取拼接key的方法:
class ModuleCollection{
...之前其他逻辑
getNamespaced(path){
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
}
最后在 installModule
中,每次收集的时候调用 getNamespaced
来获取路径:
const ns = store._modules.getNamespaced(path)
module.forEachMutations((mutation, key) => {
store.mutations[ns + key] = store.mutations[ns + key] || []
store.mutations[ns + key].push((payload) => {
mutation.call(store, module.state, payload)
})
})
module.forEachActions((action, key) => {
store.actions[ns + key] = store.actions[ns + key] || []
store.actions[ns + key].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetters((getter, key) => {
store.wrapperGetters[ns + key] = function () {
return getter(module.state)
}
})
模块动态注册
关于模块动态注册的文档地址:https://vuex.vuejs.org/zh/guide/modules.html#%E6%A8%A1%E5%9D%97%E5%8A%A8%E6%80%81%E6%B3%A8%E5%86%8C
可能我们会有这样的需求,就是有些模块需要在store初始化之后动态添加的。vuex 有一个 registerModule
的API。接下来实现一下这个api。
在class Store
中新加一个成员方法registerModule
,registerModule 会接收一个字符串或者数组作为第一个参数,为了方便处理,我们都会统一处理成数组。
第二个参数 module ,使用户传入的原始的store的模块,所以我们需要格式化成我们需要的树数据。直接调用我们之前写好的ModuleCollection
的register
方法。
registerModule(path,module){
if(typeof path === 'string'){
path = [path]
}
// 将用户传入的原始的module 转换并收集到之前的树上,因为这里调用 path 不会为空,所以会执行 register 里面的else函数里,给root 挂载
this._module.register(path,module)
// 收集完之后 进行安装module
// 因为这里最后一个参数要拿到格式化后的module,所以在 register 方法中给 传入的rawModule 挂载一个格式化后的newModule,方便这里可以取到
installModule(this,this.state,path,module.newModule)
// 在vuex官方内部,重新注册的话 会重新生成一个vue实例。
// 因为在动态注册的时候,只是解决了state。比如里面还有 computed 等,没法挂载到之前 的 _vm 实例上。
resetStoreVM(this,this.state)
}
在vuex官方内部,重新注册的话 会重新生成一个vue实例,所以还需要添加一个重新生成vue实例的方法:
resetStoreVM
function resetStoreVM (store, state) {
const computed = {}
store.getters = {}
const oldVm = store._vm
forEach(store.wrapperGetters, (getter, key) => {
computed[key] = getter
Object.defineProperty(store.getters, key, {
// get () {
get: () => { // 箭头函数绑定 this
return store._vm[key]
}
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if (oldVm) { // 销毁掉老的
Vue.nextTick(() => {
oldVm.$destroy()
})
}
}
然后将 store 初始化时候创建 Vue实例也替换成 resetStore()
。
完整代码: https://gitee.com/Nsir/my-vue/blob/master/vuex-project/src/vuex/store.js