你們知道,在開發大型vue項目時,使用vuex時不可避免的,vuex可以幫助咱們在錯綜複雜的數據流中快速拿到本身想要的數據,提升開發效率,儘管vuex沒法持久化數據,但也能夠經過插件來解決該問題,總之vuex是大型項目中百利無一害的插件。html
在上文咱們實現了一個vue-router後,咱們如今就來實現一個vuex,首先咱們從vuex的原理圖入手:前端
從原理圖咱們能夠看出,$store實例經過dispatch調用actions裏的異步方法,經過commit調用mutations裏的同步方法,並只能經過mutations改變state(這裏插一句:非嚴格模式下是能夠經過commit之外的方式改變state裏的狀態的,但在嚴格模式下,Vuex中修改state的惟一渠道就是執行 commit('xx', payload) 方法,其底層經過執行 this._withCommit(fn) 設置_committing標誌變量爲true,而後才能修改state,修改完畢還須要還原_committing變量。外部修改雖然可以直接修改state,可是並無修改_committing標誌位,因此只要watch一下state,state change時判斷是否_committing值爲true,便可判斷修改的合法性,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。)而後getters可以及時獲取state中的狀態並做出計算(實際上getters就是一個計算屬性)vue
接下來咱們來簡單作一個vuex的小demo,看看vuex到底實現了哪些功能:vue-router
咱們在store文件的index.js中這樣寫:vuex
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') },2000) } }, getters:{ //獲取乘二的值 doubleCounter(state){ return state.counter*2 }, //獲取平方值 squareCounter(state){ return state.counter*state.counter } }, modules: { } })
在Home組件中這樣寫:異步
<template> <div class="hello"> <button @click="$store.commit('add')">counter:{{$store.state.counter}}</button> <button @click="$store.dispatch('add')">async counter:{{$store.state.counter}}</button> <p>double counter:{{$store.getters.doubleCounter}}</p> <p>squareCounter:{{$store.getters.squareCounter}}</p> <h2>這是一個Home組件</h2> <p>我叫rick,歡迎來看個人文章:從0實現一個vuex</p> </div> </template>
那麼咱們的頁面大體長這樣:async
點擊counter能夠自加,點擊async counter能夠延遲兩秒自加,double counter讀出雙倍的數值,squareCounter讀出平方數值函數
接下來咱們把引入的vuex換成本身自制的vuex,來繼續實現現有的這些功能:學習
import Vuex from './kvuex'
那麼熟悉了原理圖,設計了一個簡易的vuex功能示例,接下就要來要實現咱們本身的vuex啦,咱們大體按照以下思路進行:this
1. 首先在$store上掛載dispatch,state,getters,commmit等方法是確定的
2. 其次要實現state的響應式,即state改變,依賴於state的全部屬性都要能實現自動渲染
3. 接着實現commit和dispatch的內部方法,即爲何每次commit或dispatch都能自動調用相關的方法
4. 最後實現getters,要注意爲何這個getters可以實現只讀的以及它的內部如何實現計算屬性
作一個魚骨圖方便你們理解:接下來咱們來逐步實現每一個細節
1. 掛載$store
1.1 利用install和mixin
你們還記得我在實現vue-router那篇文章中的方法嗎?此處我建議你們先看比較簡單的vue-router的實現原理再看這篇文章哦~(見個人文章:https://www.cnblogs.com/funkyou/p/14580129.html)
咱們故技重施,依然利用開發插件的方式,老規矩,複習一下vue文檔:
爲了能讓全部vue組件都能經過this.$store.xxx訪問到state,mutations等,那麼咱們就要經過全局混入的方式爲vue原型上掛載一個$store的屬性,全局混入用法見文檔:
實現代碼以下:
// use調用時會傳入Vue function install(_Vue){ // 保存Vue構造函數,插件中使用 Vue=_Vue Vue.mixin({ beforeCreate() { // 判斷當前options選項是否含有store if(this.$options.store){ // 若是含有就在原型上掛載這個store實例 Vue.prototype.$store=this.$options.store } }, }) } // 由於index.js是經過Vuex接收的,因此咱們要這樣暴露出去(實際上Vuex={Store,install,xxx}) export default{Store,install}
2. 實現state響應式
2.1:借雞生蛋
首先咱們構造一個Store實例:
class Store{
constructor(options){
}
}
其實這個options就是store中的配置項:
咱們要想實現store中的數據響應式,可否藉助現成的能實現響應式的示例來「借雞生蛋」呢?(Vue實例此時瑟瑟發抖,並大喊你不要過來呀~)沒錯,Vue,就是你了,咱們new 一個Vue實例,把data存放進去,這樣咱們的數據就能自動被Vue的defineProperty追蹤並設置set和get屬性,便可實現響應式:
// data響應式處理 this._vm= new Vue({ data:{ //把state藏進$$state中 $$state:options.state } })
2.2: 利用get和set實現只讀
咱們但願咱們的state是不能經過mutations之外的任何方式修改的,即實現只讀,那麼能夠利用get和set屬性對state進行設置:
// 存取器,store.state get state(){ console.log(this._vm); return this._vm._data.$$state } set state(v){ console.error('你沒法設置state的值') }
3.實現commit
3.1判斷方法
咱們要從mutations中判斷出當前要用的是哪一個方法,並在commit內部執行這個方法,傳入state的參數,但注意要在錯綜複雜的this環境中先把寶貴的options.mutations保存起來:
// 保存mutations this._mutations=options.mutations // 保存actions this._actions=options.actions // 保存getters this._getters=options.getters
3.2偷天換日
這裏咱們想用的是commit(type,payload)中的type對應的方法,那麼咱們可否先把這個mutations[type]方法拷貝給一個函數,再在commit方法內部執行這個函數呢?答案是可行的,這就是一種偷天換日的函數複用思想:
commit(type,payload){ // 借用mutations[type]方法 const entry= this._mutations[type] if(!entry){ console.error('unknown mutation type'); } // 執行這個mutations[type]方法並傳入state參數 entry(this.state,payload) }
4. 實現dispatch
4.1注意參數
此處實現dispatch和mutations大體相同,但要注意actions和mutations中傳入參數的不一樣:
mutations中:
actions中:
顯然這裏entry要傳入的是store實例,在constructer中用this代指:
dispatch(type,payload){ const entry=this._actions[type] if(!entry){ console.error('unknow action type') } entry(this,payload) }
4.2用bind留住this
但注意,此時commit內部的this仍是不是我想要設置的那個store實例了?看demo:
此時的this已經徹底亂套了,因此咱們還須要在commit中留住this,讓他執行的永遠是store實例,直接寫:
//這樣commit和dispatch內部的this就是當前上下文中的this,即store實例 this.commit= this.commit.bind(this) this.dispatch= this.dispatch.bind(this)
5. 實現getters
5.1計算屬性
要想實現一個只讀的getters,此處咱們依然選擇在Vue實例中設置這個computed方法:
// 定義computed選項和getters const computed={} this.getters={} this._vm= new Vue({ data:{ $$state:options.state }, computed })
5.2只讀屬性
此處咱們先保存store,隨後爲這個getters設置只讀屬性,咱們能夠用Object.defineProperty方法讓咱們能經過get讀到這個方法
5.3移花接木,變無參爲有參
接下來,咱們想借用getters裏的方法並傳入state參數,可是注意:咱們的getters方法是有參數的:
那麼咱們能夠經過Object.key拿到每一個方法的索引,再用一個fn保存當前索引下的方法,再在fn裏傳入state參數,以下:
// 保存store const store=this // 遍歷拿到索引key,並經過store._getters[key]找到這個方法 Object.keys(this._getters).forEach(key=>{ // 獲取用戶定義的getter const fn =store._getters[key] // 轉換爲computed可使用無參數形式 computed[key]=function(){ return fn(store.state) } // 爲getters定義只讀屬性 Object.defineProperty(store.getters,key,{ get:()=> store._vm[key] }) })
此時咱們打印這個fn,它便是:
或者
即getters中的方法,咱們調用了它並完美的把state傳了進去,這個方法是否是讓人拍案叫絕~
接下來是所有源碼:
//1.插件:掛載$store // 2.實現Store let Vue //保存Vue構造函數,插件中使用 class Store{ constructor(options){ console.log(options); // 保存mutations this._mutations=options.mutations // 保存actions this._actions=options.actions // 保存getters this._getters=options.getters // 定義computed選項和getters const computed={} this.getters={} // 保存store const store=this // 遍歷拿到索引key,並經過store._getters[key]找到這個方法 Object.keys(this._getters).forEach(key=>{ // 獲取用戶定義的getter const fn =store._getters[key] // 轉換爲computed可使用無參數形式 computed[key]=function(){ console.log(fn); return fn(store.state) } // 爲getters定義只讀屬性 Object.defineProperty(store.getters,key,{ get:()=> store._vm[key] }) }) // data響應式處理 this._vm= new Vue({ data:{ $$state:options.state }, computed }) //這樣commit和dispatch內部的this就是當前上下文中的this,即store實例 this.commit= this.commit.bind(this) this.dispatch= this.dispatch.bind(this) } // 存取器,store.state get state(){ console.log(this._vm); return this._vm._data.$$state } set state(v){ console.error('can not set') } commit(type,payload){ // 借用mutations[type]方法 const entry= this._mutations[type] if(!entry){ console.error('unknown mutation type'); } // 執行這個mutations[type]方法並傳入state參數 entry(this.state,payload) } dispatch(type,payload){ const entry=this._actions[type] if(!entry){ console.error('unknow action type') } entry(this,payload) } } // use調用時會傳入Vue function install(_Vue){ // 保存Vue構造函數,插件中使用 Vue=_Vue Vue.mixin({ beforeCreate() { // 判斷當前options選項是否含有store if(this.$options.store){ // 若是含有就在原型上掛載這個store實例 Vue.prototype.$store=this.$options.store } }, }) } // 由於index.js是經過Vuex接收的,因此咱們要這樣暴露出去(實際上Vuex={Store,install,xxx}) export default{Store,install}
最後看下效果:
完美實現~!若是你們想和我一塊兒學習前端交流心得指點江山,歡迎加個人vx:shq173392531