想要更好的使用一个插件,可以尝试理解其实现的方式。

当然,了解一个优秀的插件,本身也会增强自己的能力。

本文,努力从零开始实现一个简易版的vuex,期间会用到很多编程思想,希望自己越来越灵活使用。

TL;DR

  • state是响应式的,巧用Vue
  • getters用Object.defineProperty实现和state的紧密相关
  • mutations是定义commit方法,commit的this需要固定为store实例
  • actions是定义dispatch,逻辑神似mutations
  • 翻到文末可以直接看vuex.js的简易版代码

vuex 的初版样子

先可以用vue create xx创建一个项目,不带vuex的。

先看看,如果有vuex插件的main.js。

从0实现简易版的vuex_vuex

!!!特别注意

  • {state:{},mutations:...},是用户传的参数
  • store 虽然可以this.$store.state,但这个 state 不完全是用户传的 state,而是处理过的 state,这两有本质区别
  • 同样,用户传过来的其他属性,也会做处理,这样才有后期的this.$store.getters.xx等等
  • 换言之,store就是对用户传的参数做各种处理,以方便用户操作她的数据。

从这推理出vuex,应该具有的特征:

  • Vue.use表明,vuex 肯定有install方法
  • new Vuex.Store表明,vuex 导出对象里,有个Store的类
  • 每个组件内部都可以this.$store表明,需要注入$store

如果对插件一脸懵的话,可以简单看下vue 插件的入门说明

第一版vuex.js就出来了:

从0实现简易版的vuex_vuex_02

但这样,$store和store实例并没有挂钩,此时可以借助Vue.mixins的beforeCreate钩子拿到当前的 Vue 实例,从而拿到实例的$options 。

export default {  install(Vue) {
    Vue.mixin({      beforeCreate() {// 这里的this是vue的实例,其参数store就是store实例(!Vue.prototype.$store) && (Vue.prototype.$store = this.$options.store;)
      }
    });
  },
  Store
};复制代码

改进:不要轻易在原型上面添加属性,应该只在根实例有store的时候才设置$store,子实例会拿到根实例的$store

从0实现简易版的vuex_vuex_03

github源码 切换到c1分支

处理用户传的 state

store 实例的state可以出现在视图里,值变化的时候,视图也一并更新。 所以,state是被劫持的,这里投机取巧的用下Vue。

// vuex.jsclass Store {  constructor(options) {this.options = options;this.state = new Vue({ data: options.state });
  }
}复制代码
<!-- App.vue --><div id="app">
  {{ $store.state.a }}  <button @click="$store.state.a++">增加  </button></div>复制代码

github源码 切换到c2分支

!!!因为state是用Vue进行响应式,所有vuex重度依赖vue,不能脱离vue使用

处理用户传的 getters

  • 用户传的getters是一个函数集合
  • 但是实际使用中,属性值是函数的返回值
  • 属性依旧是劫持的,这边因为是函数,所以不能再投机取巧了
// vuex.jsconstructor(options) {this.options = options;this.state = new Vue({ data: options.state });if (options.getters) {      this.getters = {};      Object.keys(options.getters).forEach(key => {//   这里必须是属性劫持Object.defineProperty(this.getters, key, {          get: () => {return options.getters[key](this.state);
          }
        });
      });
    }
  }复制代码
// main.jsstate: { a: 1, b: 2 },getters: { a1(state) { return state.a + 1; } }复制代码
<!-- app.vue --><div id="app">
  {{ $store.state.a }} {{ $store.getters.a1 }}  <button @click="$store.state.a++">增加  </button></div>复制代码

从0实现简易版的vuex_vuex_04

github源码 切换到c3分支

处理 mutations

mutations,传的参数是一个函数集合的对象,使用的时候commit('函数名',payload)

代码翻译:

mutations:{  addA(state,payload){state.a+=payload}
}// 使用的时候this.$store.commit('addA',2)复制代码

由此推理出,vuex 其实写了一个commit方法。这个就很简单了,直接溜上来。

// vuex.jsclass Store {  constructor(options) {//  ...if (options.mutations) {      this.mutations = { ...options.mutations };
    }
  }  commit(mutationName, ...payload) {console.log(mutationName, ...payload);this.mutations[mutationName](this.state, ...payload);
  }
}复制代码

从0实现简易版的vuex_vuex_05

// <button @click="$store.commit('addA', 2)"> 增加 </button>const store = new Vuex.Store({  state: { a: 1, b: 2 },  getters: {a1(state) {      return state.a + 1;
    },
  },  mutations: {addA(state, num) {
      state.a += num;
    },
  },
});复制代码

github源码 切换到c4分支

处理 actions

actions和mutations是很相似的。

actions:{  // 注意!!!,这里的第一个参数是store实例
  addA({commit},payload){setTimeout(()=>{commit('addA',payload)},1000)}
}// 使用的时候this.$store.dispatch('addA',100)复制代码

这下更容易了,直接copy

  commit(mutationName, ...payload) {this.mutations[mutationName](this.state, ...payload);
  }  dispatch(actionName, ...payload) {// 注意这里是this,不是this.statethis.actions[actionName](this, ...payload);
  }复制代码

从0实现简易版的vuex_vuex_06

// <button @click="$store.dispatch('addA', 2)"> 1s后增加100 </button>const store = new Vuex.Store({  // ...
  actions: {addA(store, num) {      setTimeout(() => {
        store.commit("addA", num);
      }, 1000);
    }
  },
});复制代码

github源码 切换到c5分支

优化

  • commit做处理的时候,最好用下切片思维,这样方便修改逻辑
  • commit里面的this,最好固定执行store实例,因为这样在action那边的时候,可以直接解构赋值
  • action也一样
// vuex.jsconstructor(options){  // ...if (options.mutations) {      this.mutations = {};      Object.keys(options.mutations).forEach(mutationName => {// 切片思维,这里上下都可以加逻辑this.mutations[mutationName] = (...payload) => {
          options.mutations[mutationName](...payload);
        };
      });
    }
}// 将this始终执行store实例commit = (mutationName, ...payload) => {  this.mutations[mutationName](this.state, ...payload);
}; 
复制代码

action操作一样,不在赘述代码。

actions: {  addA({commit}, num) {// 这里可以解构了!!!setTimeout(() => {
      commit("addA", num);
    }, 1000);
  }
},复制代码

github源码 切换到c6分支。

还有模块空间的内容,考虑到篇幅较长,就不在本文继续了。

附注:vuex.js的所有代码

let Vue;class Store {  constructor(options) {this.options = options;this.state = new Vue({ data: options.state });if (options.getters) {      this.getters = {};      Object.keys(options.getters).forEach(key => {Object.defineProperty(this.getters, key, {          get: () => {return options.getters[key](this.state);
          }
        });
      });
    }if (options.mutations) {      this.mutations = {};      Object.keys(options.mutations).forEach(mutationName => {this.mutations[mutationName] = (...payload) => {
          options.mutations[mutationName](...payload);
        };
      });
    }if (options.actions) {      this.actions = {};      Object.keys(options.actions).forEach(actionName => {this.actions[actionName] = (...payload) => {
          options.actions[actionName](...payload);
        };
      });
    }
  }
  commit = (mutationName, ...payload) => {this.mutations[mutationName](this.state, ...payload);
  };
  dispatch = (actionName, ...payload) => {this.actions[actionName](this, ...payload);
  };
}export default {  install(_Vue) {
    Vue = _Vue;
    Vue.mixin({      beforeCreate() {// 这里的this是vue的实例,其参数store就是store实例const hasStore = this.$options.store;// 根实例的storehasStore
          ? (this.$store = this.$options.store)
          : this.$parent && (this.$store = this.$parent.$store);
      }
    });
  },
  Store
};复制代码