理解 vuex 實現原理

本章重點講解一下 vuex 的實現原理,vue

由於代碼中註釋比較多,顯得代碼比較冗餘,因此最好把源碼下載下來,能夠把註釋刪了看一下,其實沒有多少代碼ios

開胃小菜

Vuex是什麼?git

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,vue-router

這個狀態管理應用包含如下幾個部分:vuex

  • state,驅動應用的數據源;
  • view,以聲明方式將 state 映射到視圖;
  • actions,響應在 view 上的用戶輸入致使的狀態變化。

給出一張官方的「單向數據流」理念的簡單示意:後端

圖片加載失敗!

每個 Vuex 應用的核心就是 store(倉庫)。「store」基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)。api

Vuex 和單純的全局對象有如下兩點不一樣:數組

  • Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地獲得高效更新。(也就是所謂的MVVM)
  • 你不能直接改變 store 中的狀態。改變 store 中的狀態的惟一途徑就是顯式地提交 (commit) mutations

看圖瞭解工做原理:bash

圖片加載失敗!

若是理解了這張圖,你就能知道vuex的工做原理了app

須要注意的點:

  • 改變狀態的惟一途徑就是提交mutations
  • 若是是異步的,就派發(dispatch)actions,其本質仍是提交mutations
  • 怎樣去觸發actions呢?能夠用組件Vue Components使用dispatch或者後端接口去觸發
  • 提交mutations後,能夠動態的渲染組件Vue Components

以爲是否是少了什麼,沒錯,就是getters 下面原理實現的時候會說

原理實現

準備工做

首先把不須要的文件和代碼全刪了,經典化結構,以下:

圖片加載失敗!

App.vue代碼:

<template>
   <div>
      <!-- vuex 把狀態放到一個公共的地方,哪一個組件使用,就直接能夠從公共的地方獲取狀態 -->
   </div>
</template>
<script>
export default {
   name:'app',
}  
</script>
複製代碼

main.js代碼:

import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from 'vue-router'

Vue.config.productionTip = false

new Vue({
  name:'main',
  router, //封裝了 router-view router-link $router $route
  store,  //寫到這裏,說明所有的組件均可以使用store
  render: h => h(App)
}).$mount('#app')
複製代碼

store.js代碼:

import Vue from 'vue'
//把裏面的全刪了,本身寫

// 引入本身的寫的vuex,裏面有一個對象{install},當你use時,會自動調用這個方法
//導入vuex {install Store}
import Vuex from './vuex'

Vue.use(Vuex) 

//須要建立一個倉庫並導出
//當new的時候,給Vuex.js中傳入了一堆的東西
export default new Vuex.Store({
    state:{
        name:'Fan'
    },
    //getters中雖然是一個方法,可是用時,能夠把他看成屬性
    getters:{   // 說白了,就是vue中data中的computed
        
    },
    // 改變狀態:異步請求數據  事件 
    mutations:{

    },
    actions:{
        
    }
})
複製代碼

vuex.js文件中的代碼先不寫,下面開始寫

實現state

上面準備工做作好,接下來就實現咱們的state

vuex.js中寫以下代碼(具體說明和操做已在代碼中註釋):

//定義一個Vue,讓全局均可以使用這個Vue
let Vue;

class Store{
    //當new的時候,給Vuex.js中傳入了一堆的東西,在這裏接收須要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就能夠拿到裏面的數據了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //給每一個組件的$store上掛一個state,讓每一個組件均可以用  this.$store.state
        this.state = options.state

        //在state上面傳入一個name:'Fan'打印一下
        // console.log(this.state);    //打印結果  {name: "Fan"}
/*-------------------------------------------------------------------------------------------------*/

    }
}

//install本質上就是一個函數
const install = (_Vue)=>{
    // console.log('......');  //測試能不能調到這個方法,經測試能夠調到
    //把構造器賦給全局Vue
    Vue = _Vue;

    //混入
    Vue.mixin({
        beforeCreate() {    //表示在組件建立以前自動調用,每一個組件都有這個鉤子
            // console.log(this.$options.name) //this表示每一個組件,測試,能夠打印出mian.js和App.vue中的name main和app
            
            //保證每個組件都能獲得倉庫
            //判斷若是是main.js的話,就把$store掛到上面
            if(this.$options && this.$options.store){
                this.$store = this.$options.store
            }else{
                //若是不是根組件的話,也把$store掛到上面,由於是樹狀組件,因此用這種方式
                this.$store = this.$parent && this.$parent.$store

                //在App.vue上的mounted({console.log(this.$store)})鉤子中測試,能夠獲得store ---> Store {}
            }
        },
    })
}

//導出
export default {
    install,
    Store
}
複製代碼

這樣的話,所有的組件均可以使用this.$store.state這個方法了

實現getters

首先在store.js中的getters中定義兩個方法,用來測試:

//getters中雖然是一個方法,可是用時,能夠把他看成屬性
getters:{   // 說白了,就是vue中data中的computed
    myName(state){
        return state.name+'Jun'
    },
    myAge(){
        
    }
},
複製代碼

而後在vuex.js文件中的Store類的constructor中來寫咱們的代碼,以下:

class Store{
    //當new的時候,給Vuex.js中傳入了一堆的東西,在這裏接收須要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就能夠拿到裏面的數據了
        
/*------------------------------------state原理--------------------------------------------------------*/
        //給每一個組件的$store上掛一個state,讓每一個組件均可以用  this.$store.state
        // this.state = options.state
/*-------------------------------------------------------------------------------------------------*/

/* --------------------------------狀態響應式原理---------------------------------------------------------------- */
        // 上面那種寫法不完美,當改變數據的時候,不能動態的渲染,因此須要把data中的數據作成響應式的
        //_s在下面的 get state方法中使用
        this._s = new Vue({
            data:{
                // 只有data中的數據纔是響應式
                state:options.state
            }
        })

        
        //在state上面傳入一個name:'Fan'打印一下
        // console.log(this.state);    //打印結果  {name: "Fan"}
/* ------------------------------------------------------------------------------------------------ */

/*---------------------------------getters原理-----------------------------------------------------------*/
        //獲得倉庫中的getters,若是人家不寫getters的話,就默認爲空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一個對象,對象中是一個方法  {myName: ƒ}

        //給倉庫上面掛載一個getters,這個getters和上面的那一個getters不同,一個是獲得,一個是掛載
        this.getters = {}

        //很差理解,由於人家會給你傳多個方法,因此使用這個api處理獲得的getters,獲得一個數組
        //把store.js中的getters中再寫一個方法myAge,用來測試
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        //遍歷這個數組,獲得每個方法名
        Object.keys(getters).forEach((getter)=>{
            // console.log(getter);    //打印出  myName   myAge
            Object.defineProperty(this.getters,getter,{
                //當你要獲取getter的時候,會自動調用get這個方法
                //必定要用箭頭函數,要否則this指向會出現問題
                get:()=>{
                    console.log(this);
                    return getters[getter](this.state)
                }
            })
        })
/*-------------------------------------------------------------------------------------------------*/

    } 
    get state(){
        return this._s.state
    }  
}
複製代碼

而後在App.vue中測試:

<template>
   <div>
      <!-- vuex 把狀態放到一個公共的地方,哪一個組件使用,就直接能夠從公共的地方獲取狀態 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      console.log(this.$store);
   }
}  
</script>
複製代碼

實現mutations

先用人家的試一下:

App.vue中定義一個add方法,上面定義一個按鈕用來觸發這個方法,代碼:

<template>
   <div>
      <!-- vuex 把狀態放到一個公共的地方,哪一個組件使用,就直接能夠從公共的地方獲取狀態 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      <hr>
      {{this.$store.state.age}}
      <button @click="add()">Add</button>
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      console.log(this.$store);
   },
   methods:{
      add(){
         //commit一個mutations
         this.$store.commit('add',10)
      }
   }
}  
</script>
複製代碼

store.js中用人家的vuex:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
    state:{
        name:'Fan',
        age:10
    },
    //getters中雖然是一個方法,可是用時,能夠把他看成屬性
    getters:{   // 說白了,就是vue中data中的computed
        myName(state){
            return state.name+'Jun'
        },
        myAge(){

        }
    },
    // 改變狀態:異步請求數據  事件 
    mutations:{
        add(state,payload){
            state.age += payload
        }
    },
})

複製代碼

此次當點擊Add按鈕的時候,就能實現 加10 操做

而後本身寫:

store.js中寫上mutations,而且定義兩個方法:

// 改變狀態:異步請求數據  事件 
mutations:{
	add(state,payload){
		state.age += payload
	},
	sub(){

	}
},
複製代碼

而後在vuex.js中的類Store中實現:

class Store{
    //當new的時候,給Vuex.js中傳入了一堆的東西,在這裏接收須要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就能夠拿到裏面的數據了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //給每一個組件的$store上掛一個state,讓每一個組件均可以用  this.$store.state
        // this.state = options.state
/*----------------------------------------------------------------------------------------------------*/

/* --------------------------------狀態響應式原理---------------------------------------------------------------- */
        // 上面那種寫法不完美,當改變數據的時候,不能動態的渲染,因此須要把data中的數據作成響應式的
        //_s在下面的 get state() 方法中使用
        this._s = new Vue({
            data:{
                // 只有data中的數據纔是響應式
                state:options.state
            }
        })
        
        //在state上面傳入一個name:'Fan'打印一下
        // console.log(this.state);    //打印結果  {name: "Fan"}
/* ----------------------------------------------------------------------------------------------------------------- */

/* ----------------------------------getters原理------------------------------------------------------------- */    

        //獲得倉庫中的getters,若是人家不寫getters的話,就默認爲空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一個對象,對象中是一個方法  {myName: ƒ}

        //給倉庫上面掛載一個getters,這個getters和上面的那一個getters不同,一個是獲得,一個是掛載
        this.getters = {}

        //很差理解,由於人家會給你傳多個方法,因此使用這個api處理獲得的getters,獲得一個數組
        //把store.js中的getters中再寫一個方法myAge,用來測試
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        //遍歷這個數組,獲得每個方法名
        Object.keys(getters).forEach((getter)=>{
            // console.log(getter);    //打印出  myName   myAge
            Object.defineProperty(this.getters,getter,{
                //當你要獲取getter的時候,會自動調用get這個方法
                //必定要用箭頭函數,要否則this指向會出現問題
                get:()=>{
                    // console.log(this);
                    return getters[getter](this.state)
                }
            })
        })
/* -------------------------------------------------------------------------------------------------- */
    
/* ---------------------------------------mutatios原理----------------------------------------------------------- */
        //和getters思路差很少

        //獲得mutations
        let mutations = options.mutations || {}
        // console.log(mutations);     //{add: ƒ}

        //掛載mutations
        this.mutations = {}

        //拿到對象中的一堆方法
        Object.keys(mutations).forEach((mutation)=>{
            // console.log(mutation);  //add sub
            this.mutations[mutation] = (payload)=>{
                mutations[mutation](this.state,payload)
            }
        })

        //打印看一下,正確
        // console.log(mutations);     //{add: ƒ, sub: ƒ}
        
        //可是他比較噁心,須要實現commit,在下面實現
/* -------------------------------------------------------------------------------------------------- */

    } 

    //給store上掛一個commit,接收兩個參數,一個是類型,一個是數據
    commit(type,payload){
        //{add: ƒ, sub: ƒ}
        //把方法名和參數傳給mutations
        this.mutations[type](payload)
    }

    get state(){
        return this._s.state
    }  
}
複製代碼

App.vue中測試:

<template>
   <div>
      <!-- vuex 把狀態放到一個公共的地方,哪一個組件使用,就直接能夠從公共的地方獲取狀態 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      <hr>
      {{this.$store.state.age}}
      <button @click="add()">Add</button>
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      // console.log(this.$store);
   },
   methods:{
      add(){
         //commit一個mutations
         this.$store.commit('add',10)
      }
   }
}  
</script>
複製代碼

由於代碼比較冗餘,因此我簡化了代碼,就是把公共的方法Object.keys(obj).forEach(key => { callback(key, obj[key]) })抽離出來。

能夠下載源碼看一下,這裏就很少說了

實現actions

一樣的,在vuex.js中的類Store中實現,由於我簡化了代碼,因此總體複製下來看一下,

這裏把dispatchcommit方法換成了箭頭函數,防止this指向出現問題

//定義一個Vue,讓全局均可以使用這個Vue
let Vue;

// forEach是用來循環一個對象
const forEach = (obj, callback) => {
    // 把數組中的每個key獲得  objc[key] 
    // key  value  ----> callback
    Object.keys(obj).forEach(key => {
        callback(key, obj[key])
    })
}

class Store {
    //當new的時候,給Vuex.js中傳入了一堆的東西,在這裏接收須要用constructor
    constructor(options) {
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就能夠拿到裏面的數據了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //給每一個組件的$store上掛一個state,讓每一個組件均可以用  this.$store.state
        // this.state = options.state
/*----------------------------------------------------------------------------------------------------*/

/* ---------------------------------------狀態響應式原理--------------------------------------------------------- */
        // 上面那種寫法不完美,當改變數據的時候,不能動態的渲染,因此須要把data中的數據作成響應式的
        //_s在下面的 get state方法中使用
        this._s = new Vue({
            data: {
                // 只有data中的數據纔是響應式
                state: options.state
            }
        })
/* ----------------------------------------------------------------------------------------------------------------- */

/* ----------------------------------------getters原理------------------------------------------------------- */
        //在state上面傳入一個name:'Fan'打印一下
        // console.log(this.state);    //打印結果  {name: "Fan"}

        //獲得倉庫中的getters,若是人家不寫getters的話,就默認爲空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一個對象,對象中是一個方法  {myName: ƒ}

        //給倉庫上面掛載一個getters,這個getters和上面的那一個getters不同,一個是獲得,一個是掛載
        this.getters = {}

        //很差理解,由於人家會給你傳多個方法,因此使用這個api處理獲得的getters,獲得一個數組
        //把store.js中的getters中再寫一個方法myAge,用來測試
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        forEach(getters, (getterName, value) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return value(this.state)
                }
            })
        })
/* -------------------------------------------------------------------------------------------------- */

/* ----------------------------------------mutatios原理---------------------------------------------------------- */
        //和getters思路差很少

        //獲得mutations
        let mutations = options.mutations || {}
        // console.log(mutations);     //{add: ƒ}

        //掛載mutations
        this.mutations = {}

        forEach(mutations, (mutationName, value) => {
            this.mutations[mutationName] = (payload) => {
                value(this.state, payload)
            }
        })

        //打印看一下,正確
        // console.log(mutations);     //{add: ƒ, sub: ƒ}
        //可是他須要實現commit,在下面實現
/* -------------------------------------------------------------------------------------------------- */

/* ---------------------------------------------actions原理----------------------------------------------------- */
        //和上面兩種大同小異,很少註釋了
        let actions = options.actions || {}
        this.actions = {};
        forEach(actions, (action, value) => {
            this.actions[action] = (payload) => {
                value(this, payload)
            }
        })
/* -------------------------------------------------------------------------------------------------- */

    }
    // type是actions的類型  
    dispatch = (type, payload) => {
        this.actions[type](payload)
    }

    //給store上掛一個commit,接收兩個參數,一個是類型,一個是數據
    commit = (type, payload) => {
        //{add: ƒ, sub: ƒ}
        this.mutations[type](payload)
    }

    get state() {
        return this._s.state
    }
}

//install本質上就是一個函數
const install = (_Vue) => {
    // console.log('......');  //測試能不能調到這個方法,經測試能夠調到
    //把構造器賦給全局Vue
    Vue = _Vue;

    //混入
    Vue.mixin({
        beforeCreate() { //表示在組件建立以前自動調用,每一個組件都有這個鉤子
            // console.log(this.$options.name) //this表示每一個組件,測試,能夠打印出mian.js和App.vue中的name main和app

            //保證每個組件都能獲得倉庫
            //判斷若是是main.js的話,就把$store掛到上面
            if (this.$options && this.$options.store) {
                this.$store = this.$options.store
            } else {
                //若是不是根組件的話,也把$store掛到上面,由於是樹狀組件,因此用這種方式
                this.$store = this.$parent && this.$parent.$store

                //在App.vue上的mounted()鉤子中測試,能夠獲得store ---> Store {}
            }
        },
    })
}

//導出
export default {
    install,
    Store
}
複製代碼

mutations中添加一個異步方法:

mutations: {
	add(state, payload) {
		state.age += payload
	},
	sub() {

	},
	asyncSub(state, payload) {
		state.age -= payload
	}
},
複製代碼

store.js中寫一個actions

actions: {
	asyncSub({commit}, payload) {
		setTimeout(() => {
			commit("asyncSub", payload)
		}, 2000)
	}
} 
複製代碼

最後在App.vue中定義方法測試:

<template>
   <div>
      <!-- vuex 把狀態放到一個公共的地方,哪一個組件使用,就直接能夠從公共的地方獲取狀態 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}
      <!-- 打印出 FanJun -->
      <hr> {{this.$store.state.age}}
      <!-- 同步加 -->
      <button @click="add">Add</button>
      <!-- 異步減 -->
      <button @click="sub">Async Sub</button>
   </div>
</template>
<script>
export default {
  name: "app",
  mounted() {
    // console.log(this.$store);
    // 是異步的
    setTimeout(() => {
      this.$store.state.age = 666;
    }, 1000);
    // 是同步的
    console.log(this.$store.state);
  },
  methods: {
    add() {
      //commit一個mutations
      this.$store.commit("add", 10);
    },
    sub(){
      this.$store.dispatch("asyncSub",10)
    }
  }
};
</script>
複製代碼

刪去註釋的vuex.js代碼

其實並無多少代碼

let Vue;
const forEach = (obj, callback) => {
    Object.keys(obj).forEach(key => {
        callback(key, obj[key])
    })
}
class Store {
    constructor(options) {
        this._s = new Vue({
            data: {
                state: options.state
            }
        })
        let getters = options.getters || {}
        this.getters = {};
        forEach(getters, (getterName, value) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return value(this.state)
                }
            })
        })
        let mutations = options.mutations || {}
        this.mutations = {};
        forEach(mutations, (mutationName, value) => {
            this.mutations[mutationName] = (payload) => {
                value(this.state, payload)
            }
        })
        
        let actions = options.actions || {}
        this.actions = {};
        forEach(actions,(actionName,value)=>{
            this.actions[actionName] = (payload)=>{
                value(this,payload)
            }
        })
    }
    dispatch=(type,payload)=>{
        this.actions[type](payload)
    }
    commit=(type, payload)=>{
        this.mutations[type](payload)
    }
    get state() {
        return this._s.state
    }
}
const install = _Vue => {
    Vue = _Vue
    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.store) {
                this.$store = this.$options.store
            } else {
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}
export default { install, Store }
複製代碼

總述

由於註釋太多,顯得很複雜,因此最好把源碼下載下來,本身去嘗試寫一下

附上源碼地址:Vuex實現原理


^_<

相關文章
相關標籤/搜索