想要更好的使用一个插件,可以尝试理解其实现的方式。
当然,了解一个优秀的插件,本身也会增强自己的能力。
本文,努力从零开始实现一个简易版的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。
!!!特别注意
- {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就出来了:
但这样,$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
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>复制代码
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); } }复制代码
// <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); }复制代码
// <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 };复制代码