【VUE】手写个VUEX(二)

接上篇

  • 上篇实现了一个store的4个属性,本篇实现module,plugins,namespaced。
  • 首先看一下module功能。

store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name:'yehuo'
  },
  modules:{
    a:{
      state:{a:1}
    },
    b:{
      state:{b:2}
    }
  }
})

App.vue

<template>
  <div id="app">
    {{$store.state.name}}
    <button @click="add">sda</button>
  </div>
</template>

<script>
export default {
  methods:{
    add(){
      console.log(this.$store.state.b.b);
    }
  }
}
</script>
  • 通过打印this.$store.state.b.b能获取模块b的值。
  • 分析:
  • 由于模块里面还可能有模块,然后每个模块里面的属性跟外面属性一样,所以这里应该是要用递归的。
  • 但是直接拿到一个options不好递归,需要改变结构。
  • 可以去打印下this.$store._modules发现结构大致是这个样子:
root:
	children:
		a:{...}
		b:{...}
	state:...
	_rawmodule:...
	_raw:...
  • 首先在vuex.js里加上this._modules = new ModuleCollection(options),因为刚刚打印的叫_modules,拿到的叫ModuleCollection的实例。
  • 这样我们得创建个moduleCollection的类,在里面需要把传来的options进行组织,做成一个对象,就像上面打印的那样。
  • 由于里面要用到递归,所以类里写个函数register,然后类里面递归这个函数就行。
  • 另外还需要传个数组方便页面上用点模块点模块调用。
class ModuleCollection{
    constructor(options){
        this.register([],options)   
    }   
    register=(path,rootModule)=>{
        let module={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        }
        if(path.length==0){
            this.root=module
        }
    }
}
  • 后面操作就比较烧脑了,首先需要把孩子数据弄来。
  • 这个好理解,如果本轮操作的是根节点,那么把模块扔this.root上,如果不是根节点,那么就把模块往this.root的孩子里扔。那么就判断有没有modules,有设置这个,那么遍历递归。
class ModuleCollection{
    constructor(options){
        this.register([],options)   
    }   
    register=(path,rootModule)=>{
        let module={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        }
        if(path.length==0){
            this.root=module
        }else{//本轮不是根节点,把Path里最后一个取出来作为root的孩子的键,值为本轮做的模块
            this.root._children[path[path.length-1]]=module    
        }
        if(rootModule.modules){            
            forEach(rootModule.modules,(key,value)=>{//传入子模块
                this.register(path.concat(key),value)
            })
    }
}
  • 这里加孩子有个bug,如果是子模块里还有子模块,那么会加到同一级上。
  • 我把这个地方说详细点:
  • 我们通过遍历模块,利用concat把路径加上,代表其访问的路径,但这是一个数组。
  • 比如a里面有模块c那么到c递归时候,path里就会放入[a,c],但实际这2不是同一层的。
  • 如果要他们不同层,需要先去root里找到a然后再去a的孩子里等于c模块。
  • 所以可以发现这个路径有个特性,长度及代表其深度。
  • 但我们没法直接把模块放在某个父亲的孩子上,必须从第一个开始,沿着根节点才知道放在哪个孩子上。
  • 这里就很容易想了吧,用个循环从数组第一个走起走到最后。
  • 也有个更好的方法用reduce遍历,相当于操作单层数组。
class ModuleCollection{
    constructor(options){
        this.register([],options)   
    }   
    register=(path,rootModule)=>{
        let module={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        }
        if(path.length==0){
            this.root=module
        }else{//本轮不是根节点,把Path里最后一个取出来作为root的孩子的键,值为本轮做的模块
            let parent = path.slice(0,-1).reduce((root,cur)=>(root._children[cur]),this.root)
            parent._children[path[path.length-1]]=module    
        }
        if(rootModule.modules){            
            forEach(rootModule.modules,(key,value)=>{//传入子模块
                this.register(path.concat(key),value)
            })
        }
    }
}
  • 这个对象就算是构建好了,但其中有个问题,不同模块间,同名函数被页面触发,那么是否都触发?
 modules:{
    a:{
      state:{a:1},
      modules:{
        c:{
          mutations:{
            add(state,payload){
              console.log('xxx');
            }
          }
        }
      }
    },
    b:{
      mutations:{
        add(state,payload){
          console.log('yyy');
        }
      }
    }
  }
  • 可以验证一下,答案是都会触发。
  • 所以这个mutation,就应该是这样的结构:
this.mutations[add]=[fn,fn]
  • 触发时候把数组里的fn全执行即可。
  • 另外,每个module下有自己的state,那么mutation提交后,修改的是谁的state?
  • 可以测试下,答案是只修改自己的state。
  • 所以,我们需要把前面写的getters等方法,提取出来放到公共里,而state需要单独配置。
  • 这样我们就做一个方法installModule,传入参数为store,state,path,还有我们刚刚生成的那个路径树。installModule(this,this.state,[],this.modules.root)
  • 传入store是为了把getters等方法收集到store里,state用来收集每轮state,path用来知道本轮递归到哪了。
  • 因为每个state都是自己模块里的,所以我们找个state进行收集,收集出的大state大概就长这样:
{
	state:{
		name:xxx
		a:{	
			a:ddd,
			c:{
				c:vxxcx
			}	
		},
		b:{
			b:xxxx
		}
	}
}
  • 可以发现这个格式有个问题,模块名不能和其父级的state中变量一样名字,原版也是这样的,模块会覆盖state。
  • 这样就可以最后使用点模块点模块找到对应的东西了。
  • 所以我们把以前写的getters那些删了,使用installModule进行收集fn。
const installModule=(store,rootstate,path,rootModule)=>{
    if(path.length>0){
        let parent = path.slice(0,-1).reduce((prev,cur)=>(prev[cur]),rootstate)
        Vue.set(parent,path[path.length-1],rootModule.state)
    }
    let getters = rootModule._rawModule.getters
    if(getters){
        forEach(getters,(key,value)=>{
            Object.defineProperty(store.getters,key,{
                get(){
                    return value(rootModule.state)
                }
            })
        })
    }
    let mutations = rootModule._rawModule.mutations
    if(mutations){
        forEach(mutations,(key,value)=>{
            let tmp = store.mutations[key]||[]
            tmp.push((payload)=>{
                value(rootModule.state,payload)
            })
            store.mutations[key]=tmp
        })
    }
    let actions = rootModule._rawModule.actions
    if(actions){
        forEach(actions,(key,value)=>{
            let tmp = store.actions[key]||[]
            tmp.push((payload)=>{
                value(store,payload)
            })
            store.actions[key]=tmp
        })
    }
    forEach(rootModule._children,(key,value)=>{
        installModule(store,rootstate,path.concat(key),value)
    })
}
  • 解读下这里:
  • path的长度代表深度,大于0本轮肯定是孩子,用跟上面一样的reduce操作把父级取到。然后用vue.set操作代理,因为这个新的孩子是新增的,如果变动了不会导致视图更新,所以使用Vue.set操作。
  • 下面就是收集每轮的方法,没有什么太高深的操作,都能看懂。
  • 最后就是把每个节点孩子拿来,继续递归,路径上做个记录。
import Vue from 'vue'
import Vuex from './vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name:'yehuo'
  },
  modules:{
    a:{
      state:{yehuo:1},
      modules:{
        c:{
          state:{a:223},
          mutations:{
            add(state,payload){
              state.a+=payload
            }
          },
          actions:{
            fff({commit},payload){
              setTimeout(() => {
                commit('add',payload)
              }, 1000);
            }
          }
        }
      }
    },
    b:{
      mutations:{
        add(state,payload){
          console.log('yyy');
        }
      }
    }
  }
})
  • 可以拿来测试下,都能工作即Ok。
  • 下面实现plugins
  • 先看用法,我定义了个叫persits的函数。
const persits = (store)=>{
  store.subscribe((mutations,state)=>{
    localStorage.setItem('vuex-state',JSON.stringify(state))
  })
}
  • 然后在plugins里面加入:
  plugins:[
    persits
  ]
  • 可以发现这个实际上是个钩子,把store的this传来就可以了。另外还需要写个subscribe方法。
  • subscribe里面传入触发的mutation,还有做好的state树。
  • 前面做Installmodule的时候,我们把函数放在mutations的数组里,所以这个subscribe也要放到installModule里。
  • 先store里做个数组,push插件定义的函数,然后installModule下的commit里进行执行,存入函数,等触发进行调用。
const installModule=(store,rootstate,path,rootModule)=>{
    if(path.length>0){
        let parent = path.slice(0,-1).reduce((prev,cur)=>(prev[cur]),rootstate)
        Vue.set(parent,path[path.length-1],rootModule.state)
    }
    let getters = rootModule._rawModule.getters
    if(getters){
        forEach(getters,(key,value)=>{
            Object.defineProperty(store.getters,key,{
                get(){
                    return value(rootModule.state)
                }
            })
        })
    }
    let mutations = rootModule._rawModule.mutations
    if(mutations){
        forEach(mutations,(key,value)=>{
            let tmp = store.mutations[key]||[]
            tmp.push((payload)=>{
                value(rootModule.state,payload)
                store._subscribe.forEach((fn)=>(fn({type:key,payload},rootstate)))
            })
            store.mutations[key]=tmp

        })
    }
    let actions = rootModule._rawModule.actions
    if(actions){
        forEach(actions,(key,value)=>{
            let tmp = store.actions[key]||[]
            tmp.push((payload)=>{
                value(store,payload)
            })
            store.actions[key]=tmp
        })
    }
    forEach(rootModule._children,(key,value)=>{
        installModule(store,rootstate,path.concat(key),value)
    })
}


class Store {
    constructor(options={}){
        this.s = new Vue({
            data(){
                return {state:options.state}
            }
        })
        this._modules = new ModuleCollection(options)
        this.getters={}
        this.mutations={}
        this.actions = {}
        this._subscribe = []      
        this.subscribe=(fn)=>{
            this._subscribe.push(fn)
        }
        installModule(this,this.state,[],this._modules.root)
        this.commit=(mutationName,payload)=>{
            this.mutations[mutationName].forEach((fn)=>fn(payload))
        }
        this.dispatch=(actionName,payload)=>{
            this.actions[actionName].forEach((fn)=>fn(payload))
        }
        let plugins = options.plugins||[]
        plugins.forEach((fn)=>(fn(this)))
    }
    get state(){
        return this.s.state
    }
}
  • 使用自己的测试下,是不是每次提交都会把state树存入localStorage了,成功即ok。
  • 还有个namespaced:
  • 先说下用法:
  • 由于每个模块里都有getters mutations之类的方法,触发同名方法则会全部触发,如果某个地方设了namespaced,那么访问这个方法要在模块后面加/,比如c模块的mutations里有个fff方法,c模块上面全都没设置namespaced,只有c模块设置了,那么访问c的fff方法就是c/fff
  • 那么直接访问,不管设没设namespace,是否同名都会触发?
  • 直接给答案,触发同名函数不会触发有namespace的。
  • 这个原理很简单,就是把本来模块键名前面加上设置namespace的模块名和/
  • 所以这个方法还是写到installModule里,递归中看有没有namespce,有就生成本轮所要加的路径。在添加各个方法的属性那,做一个判断,如果有namespace,就把key换成namespace+key。
const installModule=(store,rootstate,path,rootModule)=>{
    if(path.length>0){
        let parent = path.slice(0,-1).reduce((prev,cur)=>(prev[cur]),rootstate)
        Vue.set(parent,path[path.length-1],rootModule.state)
    }
    let module=store._modules.root
    let namespace = path.reduce((pre,cur)=>{
        module = module._children[cur]
        return pre + (module._rawModule.namespaced?cur+'/':'')
    },'')
    let getters = rootModule._rawModule.getters
    if(getters){
        forEach(getters,(key,value)=>{
            if(!!namespace){
                key = namespace+key
            }
            Object.defineProperty(store.getters,key,{
                get(){
                    return value(rootModule.state)
                }
            })
        })
    }
    let mutations = rootModule._rawModule.mutations
    if(mutations){
        forEach(mutations,(key,value)=>{
            if(!!namespace){
                key = namespace+key
            }
            let tmp = store.mutations[key]||[]
            tmp.push((payload)=>{
                value(rootModule.state,payload)
                store._subscribe.forEach((fn)=>(fn({type:key,payload},rootstate)))
            })
            store.mutations[key]=tmp
        })
    }
    let actions = rootModule._rawModule.actions
    if(actions){
        forEach(actions,(key,value)=>{
            if(!!namespace){
                key = namespace+key
            }
            let tmp = store.actions[key]||[]
            tmp.push((payload)=>{
                value(store,payload)
            })
            store.actions[key]=tmp
        })
    }
    forEach(rootModule._children,(key,value)=>{
        installModule(store,rootstate,path.concat(key),value)
    })
}
  • 最后使用例子测试下,能使用即成功。

  • 另外这个写出来的namespace有点跟原生不太一样,原生在设置namespace的actions里commit的函数会去找当前模块的mutations,而我们这么写的commit就会找全局的,如果想找当前的必须commit(模块名/函数,payload)。这个问题后面有空再解决。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

业火之理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值