【VUE】手写个VUEX(二)

mac2024-12-28  21

接上篇

上篇实现了一个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)