手写一个mini版的vuex叭(超基础,超详细,超简单!)

551 阅读2分钟

本文将讲解,如何手动实现一个mini版的vuex,(本文中我们称为KStore

我们的KStore要实现的功能:

  • state
  • commit
  • dispatch
  • getters

环境搭建

这一步主要做的就是:不使用官方的vuex库,而使用我们自己的。

  • 创建项目

    vue create myapp
    npm run serve
    
  • 新建src/KStore/index.jssrc/KStore/vuex.js文件

    //src/KStore/index.js文件
    import Vue from "vue";
    import Vuex from "./vuex";
    Vue.use(Vuex);
    export default new Vuex.Store({
      state: {
        counter: 1,
      },
      getters: {
        doubleCounter(state) {
          return state.counter * 2;
        },
      },
      mutations: {
        add(state) {
          state.counter += 1;
        },
      },
      actions: {
        add({ commit }) {
          commit("add");
        },
      },
      modules: {},
    });
    
    
    // src/KStore/vuex.js文件
    class Store {
        // 2.实现state
        
        // 3.实现commit
        
        // 4.实现dispatch
        
        // 5.实现getters
    }
    function install() {
        // 1.实现install方法
    }
    export default {
      Store,
      install,
    };
    
    
    
  • 修改main.js文件

    import store from "./store";
    // 修改为
    import store from "./KStore";
    
  • home.vue中添加对vuex的使用,方便我们测试

    <template>
      <div class="home">
        <p style="font-size: 30px">
          state:{{ $store.state.counter }}
          <br />
          getters:{{ $store.getters.doubleCounter }}
          <br />
    
          <button @click="$store.commit('add')">Mutation +</button>
          &nbsp;
          <button @click="$store.dispatch('add')">Actions +</button>
        </p>
      </div>
    </template>
    
  • 看下浏览器,报错正常,接下来我们就实现我们的vuex image.png

install方法

install方法,是每个插件必备的方法也是格式,主要是给vue的原型拓展方法,我们这个也不例外。

代码变成如下这样:

let Vue;
class Store {}

function install(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      // if判断是因为我们只在根实例里面传入了store,混入的话每个组件都会执行,所以我们要加if判断
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store;
      }
    },
  });
}
export default {
  Store,
  install,
};

看下此时的浏览器,$store已经可以访问到了。

image.png

state

用法

this.$store.state.xxx

实现思路

通过this.$options接收用户传入的参数,响应式的方式通过new Vue(),因为用户不能直接修改state里面的值,只能通过commit方法来修改state,所以用get、set访问器设置访问拦截。

在上一篇文章:手写mini版的vue-router,中我们用了一种响应式方法,今天我们来用另一种new Vue()的方式,俗称借鸡生蛋。哈哈哈哈!!!

注意事项

1.说一下这边为什么要用$$state?

是因为如果你data中的变量是以$$开头的,那么Vue就不会去代理这个变量,这个机制是Vue内部的机制,记住就可以。

代码实现

let Vue;
class Store {
  constructor(options) {
    this.$options = options;
    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
    });
  }

  // 实现state
  get state() {
    return this._vm._data.$$state;
  }
  set state(val) {
    console.log("不能修改state值");
    return;
  }
}

function install(_Vue) {
  ...
}
export default {
  Store,
  install,
};

因为我们现在getters还没实现,所以要把Home.vue文件对getters的引用注释掉,否则看不到效果,会报错!!

image.png

commit

用法

this.$store.commit("add",1);

实现思路

首先我们通过commit方法中传递的参数,确定执行mutation[event]方法,再把this.state当作参数传入fn中,这就是我们实现commit的心路历程。

代码实现

let Vue;
class Store {
  constructor(options) {
    this.$options = options;
    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
    });
  }

  // 实现state
  get state() {
    return this._vm._data.$$state;
  }
  set state(val) {
    console.log("只能通过commit修改state的值");
    return;
  }

  // 实现commit方法
  commit(event, payload) {
    const fn = this.$options.mutations[event]; //找到对应mutations里面的函数
    if (!fn) {
      console.error("没有此mutation方法");
      return;
    } //判断有无此方法
    fn(this.state, payload); //传入state值,执行此方法
  }
}

function install(_Vue) {
 ...
}
export default {
  Store,
  install,
};
  • 成果演示

    点击mutation按钮,数值就会加一。如此,完美实现commit,是不是很简单!

    image.png

dispatch

用法

// actions中声明
{
    actions:{
        add({commit},payload){
            commit("add");
        }
    }
}

// 页面中调用
this.$store.dispatch("add",1);

实现思路

dispatch实现思路和commit的实现思路一样,都是通过传入的参数event,从actions里面确定执行哪个函数。

注意事项

dispatch中要调用commit,所以我们需要注意commit的执行上下文环境,通俗一点说就是this的指向

代码实现

let Vue;
class Store {
  constructor(options) {
    this.$options = options;
    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
    });
  }

  get state() {
    return this._vm._data.$$state;
  }
  set state(val) {
    console.log("只能通过commit修改state的值");
    return;
  }
  commit(event, payload) {
    ...
  }


  // 实现dispatch方法
  dispatch(event, payload) {
    const fn = this.$options.actions[event];//找到对应actions里面的函数
    if (!fn) {
      console.error("没有此mutation方法");
      return;
    }
    fn(this, payload);
  }
}

function install(_Vue) {
  ...
}
export default {
  Store,
  install,
};

如果按照以上代码实现的话,将会报如下错误:

image.png

解决办法,自然是通过callapplybind指定this指向,此处显然用bind更合适。

let Vue;
class Store {
  constructor(options) {
    this.$options = options;
    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
    });

    this.commit = this.commit.bind(this);// 重要代码,绑定this指向。

  }
  ...
}

function install(_Vue) {
  ...
}
export default {
  Store,
  install,
};
  • 成果演示

    image.png

getters

用法

this.$store.getters.xxx

实现思路

getters是只读属性,我们可以利用闭包保存对函数的引用,同时设置computed

代码实现

let Vue;
class Store {
  constructor(options) {
    this.$options = options;
   
    // 显式绑定this指向(dispatch中调用commit,commit的指向是undefined)
    this.commit = this.commit.bind(this);


    // 实现getters
    this.getters = {};
    let computed = {};
    var store = this;
    Object.keys(this.$options.getters).forEach((key) => {
      const fn = this.$options.getters[key];
      computed[key] = function () {
        return fn(store.state);
      };
      Object.defineProperty(this.getters, key, {
        get() {
          return store._vm[key];//computed里面的值,会被代理到实例上,所以直接访问_vm就可以
        },
      });
    });

    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
      computed,
    });
  }



  get state() {
    return this._vm._data.$$state;
  }
  set state(val) {
    console.log("只能通过commit修改state的值");
    return;
  }
  commit(event, payload) {
    ...
  }
  dispatch(event, payload) {
   ...
  }
}

function install(_Vue) {
  ...
}
export default {
  Store,
  install,
};

总结

上一篇写了实现一个mini版的vue-router,这一篇实现了一个mini版的vuex,其实都是为了方便以后回顾。也就顺手写的详细一点,跟大家分享一下,不是很会写文章,望见谅!

  • 最后贴上完整代码(直接可以运行)
let Vue;
class Store {
  constructor(options) {
    // 接收用户传入的参数
    this.$options = options;

    // 显式绑定this指向(dispatch中调用commit,commit的指向是undefined)
    this.commit = this.commit.bind(this);

    // 实现getters
    this.getters = {};
    let computed = {};
    var store = this;
    Object.keys(this.$options.getters).forEach((key) => {
      const fn = this.$options.getters[key];
      computed[key] = function () {
        return fn(store.state);
      };
      Object.defineProperty(this.getters, key, {
        get() {
          return store._vm[key];
        },
      });
    });

    // 响应式处理
    this._vm = new Vue({
      data() {
        return {
          $$state: options.state,
        };
      },
      computed,
    });
  }

  // 实现state
  get state() {
    return this._vm._data.$$state;
  }
  set state(val) {
    console.log("只能通过commit修改state的值");
    return;
  }

  // 实现commit
  commit(event, payload) {
    const fn = this.$options.mutations[event]; //找到对应mutations里面的函数
    if (!fn) {
      console.error("没有此mutation方法");
      return;
    } //判断有无此方法
    fn(this.state, payload); //传入state值,执行此方法
  }

  // 实现dispatch
  dispatch(event, payload) {
    const fn = this.$options.actions[event];
    if (!fn) {
      console.error("没有此mutation方法");
      return;
    }
    fn(this, payload);
  }
}

function install(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      // if判断是因为我们只在根实例里面传入了store
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store;
      }
    },
  });
}
export default {
  Store,
  install,
};