從0實現簡易版的vuex

語言: CN / TW / HK

想要更好的使用一個插件,可以嘗試理解其實現的方式。

當然,瞭解一個優秀的插件,本身也會增強自己的能力。

本文,努力從零開始實現一個簡易版的vuex,期間會用到很多編程思想,希望自己越來越靈活使用。

TL;DR

  • state是響應式的,巧用Vue
  • gettersObject.defineProperty實現和state的緊密相關
  • mutations是定義commit方法,commit的this需要固定為store實例
  • actions是定義dispatch,邏輯神似mutations
  • 翻到文末可以直接看vuex.js的簡易版代碼

vuex 的初版樣子

先可以用vue create xx創建一個項目,不帶vuex的。

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

write-router1.png

!!!特別注意

  • {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就出來了:

write-router2.png

但這樣,$storestore實例並沒有掛鈎,此時可以藉助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

write-router6.png

github源碼 切換到c1分支

處理用户傳的 state

store 實例的state可以出現在視圖裏,值變化的時候,視圖也一併更新。 所以,state是被劫持的,這裏投機取巧的用下Vue

// vuex.js
class 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.js
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);
          }
        });
      });
    }
  }
複製代碼
// main.js
state: { 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>
複製代碼

write-router1.gif

github源碼 切換到c3分支

處理 mutations

mutations,傳的參數是一個函數集合的對象,使用的時候commit('函數名',payload)

代碼翻譯:

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

複製代碼

由此推理出,vuex 其實寫了一個commit方法。這個就很簡單了,直接溜上來。

// vuex.js
class Store {
  constructor(options) {
    //  ...
    if (options.mutations) {
      this.mutations = { ...options.mutations };
    }
  }
  commit(mutationName, ...payload) {
    console.log(mutationName, ...payload);
    this.mutations[mutationName](this.state, ...payload);
  }
}
複製代碼

write-router2.gif

// <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

actionsmutations是很相似的。

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.state
    this.actions[actionName](this, ...payload);
  }
複製代碼

write-router3.gif

// <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.js
constructor(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;
        // 根實例的store
        hasStore
          ? (this.$store = this.$options.store)
          : this.$parent && (this.$store = this.$parent.$store);
      }
    });
  },
  Store
};

複製代碼