vuex之module

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方法,参数分别是storepath栈当前模块
遍历上一步的格式化树。因为格式化的是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的模块,所以我们需要格式化成我们需要的树数据。直接调用我们之前写好的ModuleCollectionregister方法。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值