手写简易vuex

1,780 阅读2分钟

Vuex

项目创建

上一篇 手写简易Vue-router 里我们手写了一个简易的vue-router,这次来挑战一下实现vuex基本功能。老样子,我们先创建一个项目,使用一下 Vuex

image.png

相关组件和vuex配置

main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false


new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

App.vue


<template>
 <div id="app">
  <div class="hello">
    <p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
    <p @click="$store.dispatch('add')">async: {{$store.state.counter}}</p>
  </div>
   </div>
</template>

<script>


export default {
  name: 'HelloWorld',

}
</script>

<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

然后是 vuex 配置 store/index.js

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    add(state) {
      state.counter++
    }
  },
  actions: {
    add({commit}) {
      setTimeout(() => {
        commit('add')
      }, 1000);
    }
  }
})

项目启动

GIFscreenShot.gif

这样就成功实现了在组件中使用vuex

下面我们的目标就是编码实现自己的vuex

手写 Vuex

文件准备

创建mystore文件

image.png

main.js

import Vue from 'vue'
import App from './App.vue'
// import store from './store'
import store from './mystore' //store指向改变

Vue.config.productionTip = false


new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

mystore/index.js

import Vue from "vue";
import Vuex from "./myvuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    add(state) {
      state.counter++;
    },
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit("add");
      }, 1000);
    },
  }
});

需求分析

和实现vue-router一样,要想实现vuex,有以下一些需求需要实现:

  • vuex作为一个插件使用,需要实现store类和install方法
  • sotre类的实现要点:
    • 需要有响应式的数据 state
    • store实例需要挂载到vue实例上
    • commit 和 dispatch 方法实现

vuex 基本结构

回想vuex使用时的步骤

  • 先引入 import Vuex from 'vuex'
  • 再安装 Vue.use(Vuex);
  • 最后实例化 new Vuex.Store({...})

和vue-router不同,实例化的并不是 vuex,而是 Vuex.Store。这说明什么,说明Vuex本身就包含了 store构造函数 和 install方法,也就是说vuex应当有着如下的基本结构

mystore/myvuex.js

let Vue

class Store{
   constructor(){

   }


}


function install(_Vue) {

}

export default {Store,install}


vue组件上挂载 $store 实例

和vue-router 一样 vue组件上添加store实例可以通过混入的办法来实现。(因为 Vue.use(Vuex) 执行在先,install方法中不能直接拿到 store实例。)、

//...class Store

function install(_Vue) {
    Vue=_Vue
    console.log(Vue)
     Vue.mixin({
       beforeCreate() {
         // 此时,上下文已经是组件实例了
         // 如果this是根实例,则它的$options里面会有store实例
         if (this.$options.store) {
           Vue.prototype.$store = this.$options.store;
           //以后就能在组件中拿到 $store
         }
       },
     });
}

响应式数据 state

需要在store类中定义响应式数据state, 组件中便能使用 $store.state, 当state发生改变时页面也能更新

let Vue
class Store{
   constructor(options){
   //保存选项
    this.$options=options;
    const state=this.$options.state || {};
    //定义响应式数据$$state
    Vue.util.defineReactive(this, "$$state", state);
   }
   //对外暴露state
   get state(){
     return this.$$state
   }
   set state(v){
       console.error('please use replaceState to reset state');
   }
}

实现commit 和 dispatch 方法

这里贴上全部代码

let Vue

class Store{
   constructor(options){

    // 保存mutations 和 actions
    this._mutations = options.mutations
    this._actions = options.actions
    this.$options=options;
    const state=this.$options.state || {};
    Vue.util.defineReactive(this, "$$state", state);
    //为避免this的指向出现问题,可以将其绑定到当前store实例
    this.commit = this.commit.bind(this)
    this.dispatch=this.dispatch.bind(this)

   }

   commit(type,payload){
     //根据type找到用户定义的mutation方法
     const entry = this._mutations[type];
     if(entry){
         entry(this.state,payload)
     }
   }

   dispatch(type,payload){
       const entry = this._actions[type];
       console.log(`entry`, entry);
       if (entry) {
         entry(this, payload);
       }
   }

   get state(){
     return this.$$state
     
   }

   set state(v){
       console.error('please use replaceState to reset state');
   }


}

function install(_Vue) {
    Vue=_Vue
     Vue.mixin({
       beforeCreate() {
         // 此时,上下文已经是组件实例了
         // 如果this是根实例,则它的$options里面会有store实例
         if (this.$options.store) {
           Vue.prototype.$store = this.$options.store;
         }
       },
     });
}

export default { Store, install };

GIF录屏2.gif

大功告成,vuex的基本功能就实现了,是不是很简单,大家可以一起动手试一试

实现getters

现在我们更近一步,来尝试实现getters

app.vue

<template>
 <div id="app">
  <div class="hello">
    <p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
    <p @click="$store.dispatch('add')">async: {{$store.state.counter}}</p>
    <p >doubleCounter: {{$store.getters.doubleCounter}}</p>
  </div>
   </div>
</template>

<script>

export default {
  name: 'HelloWorld',
}
</script>
//...

mystore/index.js


import Vue from "vue";
import Vuex from "./myvuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    counter: 0,
  },
  //..mutation,action 配置
  getters: {
    doubleCounter(state) {
      return state.counter * 2;
    },
  },
});

数据代理

我们知道getters 用于获取 state 里的数据,如果要获取的数据并没有发生变化的话,就会返回缓存的数据。

等一下,缓存数据?有没有想到什么,是不是很像计算属性,那么我们能不能通过vue的实例来代理数据呢,做法如下:

mystore/myvuex.js

let Vue

class Store{
   constructor(options){

    // 保存mutations 和 actions
    this.$options=options;
    const state=this.$options.state || {};
     
    // Vue.util.defineReactive(this, "$$state", state); 
    // 新建vue来代理数据
    this._vm=new Vue({
      data:{
        $$state:state
      }
    })
    
    //绑定commit和dispatch的指向
    
    
   }

  //commit 和 dispatch实现

   get state(){
    //  return this.$$state
    // 通过vue代理state
    return this._vm._data.$$state
     
   }

   set state(v){
       console.error('please use replaceState to reset state');
   }
}

通过计算属性代理getters

我们知道 computed中的函数是无参数的,而getters在计算时候需要出入state,所以还要做高一级的函数封装


let Vue

class Store{
   constructor(options){

    // 保存mutations 和 actions
    this.$options=options;
    const state=this.$options.state || {};
    //保存传入的getters配置
    this._wrapedGetters=options.getters;
    this.getters={}
    //定义computed对象
    const computed={}
    //避免this指向出错
    const store=this;
    //本例中 ['doubleCounter']  key=> doubleCounter
    Object.keys(this._wrapedGetters).forEach((key) => {
      const fn = this._wrapedGetters[key];
      //computed:{
      // doubleCounter:function(){ return doubleCounter(store.state)}
      //}
      //再用一层函数包裹封装
      computed[key] = function(){
        return fn(store.state)
      }
      //外界访问 $store.getters.doubleCounter, 指向 store._vm 中对应代理的computed属性
      Object.defineProperty(store.getters,key,{
        get:function(){
          return store._vm[key]
        }
      })
    });


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

     //绑定commit和dispatch的指向

   }

   //...
}

doubleCounter.gif

全部myVuex代码

let Vue

class Store{
   constructor(options){

    // 保存mutations 和 actions
    this._mutations = options.mutations
    this._actions = options.actions
    this.$options=options;
    const state=this.$options.state || {};
    
    this._wrapedGetters=options.getters;
    this.getters={}
    const computed={}
    const store=this;

    Object.keys(this._wrapedGetters).forEach((key) => {
      const fn = this._wrapedGetters[key];
      computed[key] = function(){
        return fn(store.state)
      }

      Object.defineProperty(store.getters,key,{
        get:function(){
          return store._vm[key]
        }
      })
    });


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



    // Vue.util.defineReactive(this, "$$state", state);
    
    this.commit = this.commit.bind(this)
    this.dispatch=this.dispatch.bind(this)

   }

   commit(type,payload){
     console.log(this)
     const entry = this._mutations[type];
     if(entry){
         entry(this.state,payload)
     }
   }

   dispatch(type,payload){
       const entry = this._actions[type];
       console.log(`entry`, entry);
       if (entry) {
         entry(this, payload);
       }
   }

   get state(){
    //  return this.$$state
    return this._vm._data.$$state
     
   }

   set state(v){
       console.error('please use replaceState to reset state');
   }


}


function install(_Vue) {
    Vue=_Vue
     Vue.mixin({
       beforeCreate() {
         // 此时,上下文已经是组件实例了
         // 如果this是根实例,则它的$options里面会有store实例
         if (this.$options.store) {
           Vue.prototype.$store = this.$options.store;
         }
       },
     });
}

export default { Store, install };