大家知道,在開發大型vue專案時,使用vuex時不可避免的,vuex能夠幫助我們在錯綜復雜的資料流中快速拿到自己想要的資料,提高開發效率,盡管vuex無法持久化資料,但也可以通過插件來解決該問題,總之vuex是大型專案中百利無一害的插件,

在上文我們實作了一個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就是一個計算屬性)
接下來我們來簡單做一個vuex的小demo,看看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') },2000) } }, getters:{ //獲取乘二的值 doubleCounter(state){ return state.counter*2 }, //獲取平方值 squareCounter(state){ return state.counter*state.counter } }, modules: { } })
在Home組件中這樣寫:
<template> <div > <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>
那么我們的頁面大致長這樣:

點擊counter可以自加,點擊async counter可以延遲兩秒自加,double counter讀出雙倍的數值,squareCounter讀出平方數值

接下來我們把引入的vuex換成自己自制的vuex,來繼續實作現有的這些功能:
import Vuex from './kvuex'
那么熟悉了原理圖,設計了一個簡易的vuex功能示例,接下就要來要實作我們自己的vuex啦,我們大致按照如下思路進行:
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/270869.html
標籤:JavaScript
