接上篇
- 上篇实现了一个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)。这个问题后面有空再解决。