理解vuex中常用的几个问题
- 模块的局部状态
- namespaced注册的命名空间
- strict严格模式的开启
- 动态注册模块
- 辅助函数的实现
一.vuex中的模块实现
- 我们在项目中一般多层嵌套module,结构如下
{
"a": {
"state": {
"age": 20,
"sex": "男"
},
"getters": {},
"mutations": {},
"modules": {
"c": {
"state": {
"ageC": 20,
"sexC": "男"
},
"getters": {},
"mutations": {}
}
}
}
}
但上面的结构不好处理,对于我们期望而言,我们更加期待于是一种树形的结构来表达数据的嵌套,我们更期待于数据是以下形式的
this.root = {
_raw: 用户定义的模块,
state: 当前模块自己的状态,
_children: { // 孩子列表
a: {
_raw: 用户定义的模块,
state: 当前模块自己的状态,
_children: { // 孩子列表
c: {}
}
},
b: {
}
}
}
- 第一步处理用户传入的数据,新建一个ModuleCollect类用来处理数据
//store.js
import ModuleCollect from './module-collect'
class Store {
constructor(options) {
this._modules = new ModuleCollect(options)
}
}
export default Store
2.对数据进行处理,期待得到我们上述描述的数据结构
//ModuleCollect.js
class ModuleCollect {
constructor(options) {
//对数据进行格式化操作
this.root = null
this.resister([],options)
}
resister(path,rawModule){
let newModule = {
_raw:rawModule,
_children:{},
state:rawModule.state
}
if(path.length === 0) {
this.root = newModule
}
if(rawModule.modules){
forEach(rawModule.modules,(module,key) => {
//这里可以得到对应的模块和对应的key值
console.log(module,key)
this.resister(path.concat(key),module) //递归循环当前值
})
}
}
}
export const forEach = (obj,fn)=>{
Object.keys(obj).forEach((key)=>{
fn(obj[key],key)
})
}
export default ModuleCollect
3.在上面代码中,我们递归循环去遍历所有值,如果我们存在多层嵌套关系,那么其中的path.length就不会为空,那么我们就得把对应的值,插入到对应位置,比如[a,c]值,我们得把a插入到root中,得把c插入到a中,那么我们想清楚了就来进行下一步逻辑
- ps因为上面代码中forEach用到的地方会很多,那么我们就把他提取到工具函数中
//util.js
export const forEach = (obj,fn)=>{
Object.keys(obj).forEach((key)=>{
fn(obj[key],key)
})
}
提取完成,我们再来完成后面的逻辑
import { forEach } from './util'
class ModuleCollect {
constructor(options) {
//对数据进行格式化操作
this.root = null
this.resister([],options)
}
resister(path,rawModule){
if(path.length === 0) {
this.root = newModule
}else {
// [a,c]
//找父亲,新增代码,用来将对应的值添加到对应的位置
let parent = path.slice(0,-1).reduce((memo,current) => {
return memo._children[current]
},this.root)
parent._children[path[path.length - 1]] = newModule
}
}
}
export default ModuleCollect
4.通过上面的逻辑后,我们的结构树就处理完成了,可以我们可以在控制台打印出来看到对应的树,就是我们期待的树的嵌套关系
5.优化上述代码,为了便于拓展,我们将moudle中的操作提取出来,所以我们新建一个moodule的类,便于所有的moudule操作的方法都写在里面,并且便于我们对其拓展方法
//module.js
class Module {
constructor(rawMoudle) {
this._raw = rawMoudle
this._children = {}
this.state = rawMoudle.state
}
getChild(childName) {
return this._children[childName]
}
addChild(childName, module) {
this._children[childName] = module
}
export default Module
然后在改写moudle-collection.js中的方法
import { forEach } from './util'
import Module from "./moudle";
class ModuleCollect {
constructor(options) {
//对数据进行格式化操作
this.root = null
this.resister([],options)
}
resister(path,rawModule){
let newModule = new Module(rawModule)
if(path.length === 0) {
this.root = newModule
}else {
//改写部分
let parent = path.slice(0,-1).reduce((memo,current) => {
return memo.getChild(current) //通过类中的方法找到对应要素
},this.root)
parent.addChild(path[path.length - 1],newModule)
}
if(rawModule.modules){
forEach(rawModule.modules,(module,key) => {
this.resister(path.concat(key),module)
})
}
}
}
export default ModuleCollect
改写完成后,我们发现我们所以针对于module的操作都可以放在类中进行,这样既方便了我们拓展类,也实现了代码的解耦,我们打印出来最终结果可以看到,那我们的第一步处理传入用户数据的方法就完成了
- 递归处理用户传入的数据,处理成我们常用的属性结构
- 如果将对应模块添加到对应的位置,找父亲,[a,c],将c插入到a中
- 抽离module,便于拓展方法和阅读
二.vuex中的模块收集
- 没有namespace的时候 getters都放在根上
- actions,mutations 会被合并数组
1.回到store类中
import { Vue } from './install'
import ModuleCollect from './module-collect'
function installModule(){
}
class Store {
constructor(options) {
this._modules = new ModuleCollect(options)
this.wrapperGetters = {}
this.getters = {} // 我需要将模块中的所有的getters,mutations,actions进行收集
this.mutations = {}
this.actions = {}
// 没有namespace的时候 getters都放在根上 ,actions,mutations 会被合并数组
let state = options.state
installModule(this,state,[],this._modules.root)
}
}
export default Store
2.installModule中注册模块的方法,我们将所有getters,mutaions,actions都要收集到store类中
function installModule(store,path,module){
forEach(module._raw.getters,(fn,key) =>{
//挂载到getters上面
store.wrapperGetters[key] = function (){
return fn.call(store,module.state)
}
})
forEach(module._raw.mutations,(fn,key) =>{
store.mutations[key] = store.mutations[key] || []
//挂载到getters上面
store.mutations[key].push((payload) => {
return fn.call(store,module.state,payload)
})
})
forEach(module._raw.actions,(fn,key) =>{
store.actions[key] = store.actions[key] || []
//挂载到getters上面
store.actions[key].push((payload) => {
return fn.call(store,module.state,payload)
})
})
}
完成installModule后,发现其中很多写法都是重复的调用,而且moudle的方法我们都应该放在moudle类中,于是我们优化上面的写法
//moudle.js中新增
forEachGetter(cb){
this._raw.getters && forEach(this._raw.getters,cb)
}
forEachMutation(cb) {
this._raw.mutations && forEach(this._raw.mutations, cb)
}
forEachAction(cb) {
this._raw.actions && forEach(this._raw.actions, cb)
}
在用来改写installModule的方法
function installModule(store,path,module){
module.forEachGetter((fn,key) => {
store.wrapperGetters[key] = function (){
return fn.call(store,module.state)
}
})
module.forEachMutation((fn,key) => {
store.mutations[key] = store.mutations[key] || []
store.mutations[key].push(payload => {
return fn.call(store,module.state,payload)
})
})
module.forEachAction((fn,key) => {
store.actions[key] = store.actions[key] || []
store.actions[key].push((payload) => {
return fn.call(store,store,payload)
})
})
}
在循环递归installModule方法,注册所有模块
//module.js新增
..........
forEachChildren(cb){
this._children && forEach(this._children,cb)
}
function installModule(store,rootState,path,module){
//新增
//在递归循环childen并且注册所有元素
module.forEachChildren((child,key)=> {
installModule(store,rootState,path.concat(key),child)
})
}
最后,注册所有模块完成,store.js中完整代码
import { Vue } from './install'
import ModuleCollect from './module-collect'
import { forEach } from './util'
function installModule(store,rootState,path,module){
module.forEachGetter((fn,key) => {
store.wrapperGetters[key] = function (){
return fn.call(store,module.state)
}
})
module.forEachMutation((fn,key) => {
store.mutations[key] = store.mutations[key] || []
store.mutations[key].push(payload => {
return fn.call(store,module.state,payload)
})
})
module.forEachAction((fn,key) => {
store.actions[key] = store.actions[key] || []
store.actions[key].push((payload) => {
return fn.call(store,store,payload)
})
})
//在递归循环childen并且注册所有元素
module.forEachChildren((child,key)=> {
installModule(store,rootState,path.concat(key),child)
})
}
class Store {
constructor(options) {
this._modules = new ModuleCollect(options)
this.wrapperGetters = {}
this.getters = {} // 我需要将模块中的所有的getters,mutations,actions进行收集
this.mutations = {}
this.actions = {}
const computed = {}
// 没有namespace的时候 getters都放在根上 ,actions,mutations 会被合并数组
let state = options.state
installModule(this,[],this._modules.root)
// console.log(this.wrapperGetters,this.mutations,this.actions)
}
}
export default Store
输入查看是否是我们想要的结果,我们可以看到,这样就达到了我们所期待的结果,getters都放在根上,mutations和actions相同的就会合并成为一个数组
3.state状态收集处理
- 上面可以看到,我们处理getters,mutations和actions的收集
- 还有一个重要的属性,state状态的收集
- 我们知道state中状态mutaions来改变的,所以我们还要处理commit函数的实现 那么我们知道了以上功能了,那么我们来实现自己的代码
1.先收集state
//store中新增
constructor(options) {
this.actions = {};
let state = options.state;
//传入state参数
installModule(this, state, [], this._modules.root);
this._vm = new Vue({
data: {
$$state: state
},
});
}
get state(){
return this._vm._data.$$state
}
2.找到对应的父模块,将状态声明上去
function installModule(store,rootState,path,module){
//新增代码
//module.state => 放到rootState对应的儿子里
if(path.length > 0) {
//{ age:20,sex:'男',a:aState}
let parent = path.slice(0,-1).reduce((memo,current) =>{
return memo[current]
},rootState)
Vue.set(parent,path[path.length - 1],module.state)
}
}
打印出来rootState可以看到,state之间的关系就被定义好了
3.处理getters中的缓存功能,遍历我们所收集wrapperGetters,将其挂载到computed中,在实现commit和dispath方法,就是传入对应的类型,找到对应的数组遍历执行,所以这就解释了,为什么相同的
constructor(options) {
this.wrapperGetters = {}
const computed = {}
//新增代码
forEach(this.wrapperGetters,(getter,key) => {
computed[key] =getter
Object.defineProperty(this.getters,key,{
get:() => this._vm[key]
})
})
this._vm = new Vue({
data:{
$$state:state
},
computed
})
}
get state(){
return this._vm._data.$$state
}
commit = (mutaionName,payload) => {
this.mutations[mutaionName] && this.mutations[mutaionName].forEach(fn => fn(payload))
}
dispath = (actionName,payload) => {
this.actions[actionName] && this.actions[actionName].forEach(fn => fn(payload))
}
我们就完成了store中的依赖收集功能,贴上完整的store中的代码
import { Vue } from './install'
import ModuleCollect from './module-collect'
import { forEach } from './util'
function installModule(store,rootState,path,module){
//module.state => 放到rootState对应的儿子里
if(path.length > 0) {
//{ age:20,sex:'男',a:aState}
let parent = path.slice(0,-1).reduce((memo,current) =>{
return memo[current]
},rootState)
Vue.set(parent,path[path.length - 1],module.state)
}
module.forEachGetter((fn,key) => {
store.wrapperGetters[key] = function (){
return fn.call(store,module.state)
}
})
module.forEachMutation((fn,key) => {
store.mutations[key] = store.mutations[key] || []
store.mutations[key].push(payload => {
return fn.call(store,module.state,payload)
})
})
module.forEachAction((fn,key) => {
store.actions[key] = store.actions[key] || []
store.actions[key].push((payload) => {
return fn.call(store,store,payload)
})
})
//在递归循环childen并且注册所有元素
module.forEachChildren((child,key)=> {
installModule(store,rootState,path.concat(key),child)
})
}
class Store {
constructor(options) {
this._modules = new ModuleCollect(options)
this.wrapperGetters = {}
this.getters = {} // 我需要将模块中的所有的getters,mutations,actions进行收集
this.mutations = {}
this.actions = {}
const computed = {}
// 没有namespace的时候 getters都放在根上 ,actions,mutations 会被合并数组
let state = options.state
installModule(this,state,[],this._modules.root)
forEach(this.wrapperGetters,(getter,key) => {
computed[key] =getter
Object.defineProperty(this.getters,key,{
get:() => this._vm[key]
})
})
this._vm = new Vue({
data:{
$$state:state
},
computed
})
console.log(this.getters,this.mutations)
}
get state(){
return this._vm._data.$$state
}
commit = (mutaionName,payload) => {
this.mutations[mutaionName] && this.mutations[mutaionName].forEach(fn => fn(payload))
}
dispath = (actionName,payload) => {
this.actions[actionName] && this.actions[actionName].forEach(fn => fn(payload))
}
}
export default Store
总结
- 针对于依赖收集功能,我们就是将用户传入的参数进行处理,挂载到store中
- state中响应式,就是取来自data中已经绑定好的数据
- getters中的缓存数据功能,依赖于computed
三.vuex中命名空间的实现
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
1.首先我们得在自己的模块中标识带上namespaced没有
class Module {
//新增代码
//用于标识自己写了namespaced没有
get namespaced(){ //module.namespaced
return !!this._raw.namespaced
}
}
2.得在ModuleCollect类中添加getNamespace方法,获取到对应的路径
class ModuleCollect {
//新增代码
getNamespace(path){
//[a,b,c]
//返回一个字符串 a/b/c
let root = this.root
let ns = path.reduce((ns,key) => {
let module = root.getChild(key) //获取到当前模块
root = module
return module.namespaced ? ns + key + '/':ns
},'')
return ns
}
}
3.在收集依赖的时候,收集到对应的模块下面,改写installModule注册模块方法
function installModule(store,rootState,path,module){
//获取到moduleCollection类的实例,得到路径
let ns = store._modules.getNamespace(path)
//module.state => 放到rootState对应的儿子里
if(path.length > 0) {
//{ age:20,sex:'男',a:aState}
let parent = path.slice(0,-1).reduce((memo,current) =>{
return memo[current]
},rootState)
Vue.set(parent,path[path.length - 1],module.state)
}
module.forEachGetter((fn,key) => {
store.wrapperGetters[ns+ key] = function (){
return fn.call(store,module.state)
}
})
module.forEachMutation((fn,key) => {
store.mutations[ns+ key] = store.mutations[ns+ key] || []
store.mutations[ns+ key].push(payload => {
return fn.call(store,module.state,payload)
})
})
module.forEachAction((fn,key) => {
store.actions[ns+ key] = store.actions[ns+ key] || []
store.actions[ns+ key].push((payload) => {
return fn.call(store,store,payload)
})
})
//在递归循环childen并且注册所有元素
module.forEachChildren((child,key)=> {
installModule(store,rootState,path.concat(key),child)
})
}
总结
- 在对应的module中判断是否存在namespaced标识
- 在注册时,根据模块注册路径调整命名
四.模块动态注册
在 store 创建之后,你可以使用 store.registerModule
1.动态注册模块,调用registerModule注册方法,在调用installModule安装方法
registerModule(path,module){
if(typeof path === 'string') path = [path]
//调用module下面的register方法
this._modules.resister(path,module)
//安装模块
installModule(this,this.state,path, module)
}
看起来好像很完美,我们已经调用了这两个方法,那么当前moudule就应该已经注册上去了,那么其中有什么问题没有呢?我们传入的module,跟我们上面注册的module是不一样,上面的moudule是通过Module类来生成的,那么找到问题了,我们来改造一下逻辑
class ModuleCollect {
resister(path,rawModule){
let newModule = new Module(rawModule)
//构建出来一个newModule,然后在将构建的newModule赋值在当前rawModule的一个属性上面
rawModule.newModule = newModule
}
}
在对registerModule中的传参改写
registerModule(path,module){
if(typeof path === 'string') path = [path]
//调用module下面的register方法
this._modules.resister(path,module)
//安装模块
installModule(this,this.state,path, module.newModule)
}
上面这种写法有什么问题呢?vuex内部重新注册的话,会重新生成实例,虽然重新安装了,值解决了状态问题,但是computed就丢失了,所以我们就有了resetVM方法,用来重新生成响应
function resetVm(store,state){
let oldVm = store._vm
const computed = {}
store.getters = {}
forEach(store.wrapperGetters,(getter,key) => {
computed[key] =getter
Object.defineProperty(store.getters,key,{
get:() => store._vm[key]
})
})
store._vm = new Vue({
data:{
$$state:state
},
computed
})
if(oldVm){ // 重新创建实例后,需要将老的实例卸载掉
Vue.nextTick(() => oldVm.$destroy())
}
}
完成registerModule方法
registerModule(path,module){
installModule(this,this.state,path, module.newModule)
//新增代码
resetVM(this, this.state); // 销毁重来
}
总结:
- 动态注册模块本质就是再次调用resister注册和installModule安装模块方法
- 注意其中的module是Module类生成的实例
- 记得重新生成响应式实例
五丶strict严格模式
- 在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
- 保证只有在mutations下面的变更下才是合理的,那么我们是否可以用一个变量来控制,保证只有在mutaions中方法执行时,该变量才为true,其他时候为false,那我们我们思考清楚后,来实现我们的代码
1.定义好_committing变量来控制是否是同步执行
class Store {
constructor(options) {
this._committing = false //默认不是在mutaion中更改
this.strict = options.strict
installModule(this,state,[],this._modules.root)
resetVm(this,state)
}
_withCommittting(fn){
this._committing = true
fn() //函数时同步的 获取_commiting就是true,如果是异步的那么就会变成false
,就会打印日志
this._committing = false
}
2.在resetVMf方法中监听数据的变化,每次监听到数据的变化,通过_committing的值来判断是否提示预警
function resetVm(store,state){
//新增代码
if(store.strict){
store._vm.$watch(() => store._vm._data.$$state,() => {
console.assert(store._committing,' do not mutate vuex store state outside mutation handlers')
},{deep:true,sync:true})
}
}
3.在mutations执行的地方,先执行_withCommittting方法
function installModule(store,rootState,path,module){
if(path.length > 0) {
//新增代码
store._withCommittting(() => {
Vue.set(parent,path[path.length - 1],module.state)
})
}
module.forEachMutation((fn,key) => {
store.mutations[ns+ key] = store.mutations[ns+ key] || []
store.mutations[ns+ key].push(payload => {
//新增代码
store._withCommittting(() => {
fn.call(store,module.state,payload)
})
})
})
}
- 当_withCommittting方法执行的时候,_committing变量就会变成true,在同步执行mutations的方法,这时观测到_committing为true,所以监听断言就不会执行,当函数是在异步执行的时候,_committing为false,那么就会预警提示
六丶辅助函数的实现
1.mapState的实现,我们假设state中有age,getters中doubleAge两个变量,我们正常在computed中取值如下,从本质上就是返回的对象
computed:{
name(){
return this.$store.state.age
},
myAge(){
return this.$store.getters.doubleAge
}
}
//改写为mapState和mapGetters函数
computed:{
...mapState(["age"]), // mapXxxx返回的是对象
...mapGetters(["doubleAge"]),
}
那我们知道mapState返回的其实就是对象,所以我们可以在computed中使用拓展运算符
function mapState(stateList) {
let obj = {};
for (let i = 0; i < stateList.length; i++) {
let stateName = stateList[i];
obj[stateName] = function () {
return this.$store.state[stateName];
};
}
return obj;
}
function mapGetters(gettersList) {
let obj = {};
for (let i = 0; i < gettersList.length; i++) {
let getterName = gettersList[i];
obj[getterName] = function () {
return this.$store.getters[getterName];
};
}
return obj;
}
同理可得mapMutations和mapActions
function mapMutations(mutationList) {
let obj = {};
for (let i = 0; i < mutationList.length; i++) {
obj[mutationList[i]] = function (payload) {
this.$store.commit(mutationList[i], payload);
};
}
return obj;
}
function mapActions(actionList) {
let obj = {};
for (let i = 0; i < actionList.length; i++) {
obj[actionList[i]] = function (payload) {
this.$store.dispatch(actionList[i], payload);
};
}
return obj;
}
最后附上gitee地址:gitee.com/skygoodshow…