手写Vuex(详细篇)

73 阅读10分钟

理解vuex中常用的几个问题

  1. 模块的局部状态
  2. namespaced注册的命名空间
  3. strict严格模式的开启
  4. 动态注册模块
  5. 辅助函数的实现

一.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: {

         }
     }

 }
  1. 第一步处理用户传入的数据,新建一个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.通过上面的逻辑后,我们的结构树就处理完成了,可以我们可以在控制台打印出来看到对应的树,就是我们期待的树的嵌套关系

image.png

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,便于拓展方法和阅读

image.png

二.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相同的就会合并成为一个数组

image.png

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之间的关系就被定义好了

image.png

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)
    })
}

image.png 总结

  • 在对应的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…