摸魚間隙來實現一個 Vuex

開篇圖

前言

若是你用過 Vue,那麼 Vuex 一定是你使用過程當中沒法繞過的一道坎。javascript

用過之後你有沒有想過,前端

他的內部原理到底是怎麼樣的呢?vue

今天咱們就經過對其簡單的實現,java

一塊兒來探究它內部的原理。git

這裏就默認你們已經用過 Vuex 而且對它的操做還比較熟悉了,github

若是還不熟悉的同窗能夠異步 官網教程vuex

先說說雙向綁定

用過的同窗應該都會對 Vuex 強大的數據管理能力印象深入,編程

那麼操做 Vuex 中的數據是怎麼讓渲染視圖實時更新的呢?緩存

沒錯,這時候老大哥 Vue 就要出場了。框架

出場

編程是門黑魔法,下面這種操做不知道在你的代碼中有沒有出現過,

(若是你不知道,權當開個眼界嘿嘿。

// 建立一個全新的 Vue 實例
const bus = new Vue()

// 將其掛載到當前項目實例
this.$bus = bus

// 進行通訊
this.$bus.$emit('call', '呼叫')

this.$bus.$on('call', () => alert('收到'))
複製代碼

這是一個使用 Vue 實例進行全局通訊的例子,其優勢和缺點都很是的明顯,

可是今天的重點並非要講這個。

不知道你們有沒有注意到咱們在實現通訊的過程當中實際使用了 Vue 中的 $emit$on 的方法,

這給了咱們啓發,

咱們能不能讓 Vue 中已有的雙向綁定爲咱們所用呢?

說幹就幹!

開始表演

Vuex 功能測試準備

在開始搭建咱們本身的 Vuex 前,咱們先預先設定好像要實現的功能,

方便咱們在開發過程當中隨時進行測試。

最後的效果以下:

測試界面

左邊按鈕中的數字依賴於倉庫中的 count 變量,且每次點擊都加1。

接着咱們編寫好調用 Vuex 的代碼,使用方法同官方庫:

import Vue from 'vue'
import MyVuex from './myVuex'

Vue.use(MyVuex)

const store = new MyVuex.Store({
    state: {
        count: 1
    },
    getters: {
        getCount(state) {
            return state.count
        },
        getOne(state) {
            return 1
        }
    },
    mutations: {
        doCount(state, data) {
            state.count = data
        }
    },
    actions: {
        doCount({ commit }, data) {
            commit('doCount', data)
        },
        doCountDouble({ state, commit }) {
            commit('doCount', state.count * 2)
        }
    }
})

export default store
複製代碼

萬事具有以後就能夠正式開始開發了!

Vuex 的核心主要有那麼四部分:

  • state
  • getters
  • mutations
  • actions

下面咱們來一一對這些部分進行剖析吧

使用 Vue 雙向綁定構建 state

上面咱們提到過,數據處理的核心其實仍是利用了 Vue 的雙向綁定,

聽從着這個思路咱們能夠搭出整個庫的雛形:

export class Store {
    constructor(options = {}, Vue) {
        // 沒有 Vue 時先裝上
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        // 獲取配置
        const { state = {} } = options
        // 新建 Vue 實例響應式存儲
        resetStoreVM(this, state)
    }
    get state() {
        return  this._vm._data.$$state
    }
}

// 新建 Vue 實例
function resetStoreVM (store, state) {
    // 先看有沒有舊實例
    const oldVm = store._vm
    
    if (oldVm) {
        Vue.destroy(oldVm)
    }
    // store.getters = {}

    store._vm = new Vue({
        data: {
          $$state: state
        },
    })
}
複製代碼

這時候咱們把 Store 的實例打印出來,就能看到咱們的 state 已經被加載好了。

state的值

實現 getters

上面咱們已經成功加載好了 state

可是通常而言並不推薦直接取值,而是最好經過 getters 進行值的獲取,方便進行二次加工。

在實現 getters 以前,咱們先來看看文檔中的說明。

getters文檔說明

文檔中說明 getters 的值是有緩存優化策略的,可是咱們這裏爲了方便就直接每次都使用 新計算 的值,

若是有感興趣的同窗可在源碼搜索 store._makeLocalGettersCache 的相關代碼。

如今咱們的代碼變成了這個樣子:

export class Store {
    constructor(options = {}, Vue) {
        // 沒有 Vue 時先裝上
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        // 獲取配置
        const { state = {}, getters = {}, } = options

        this.getters = Object.create(null)

        // 裝載 getters
        forEachValue(getters, (fn, type) => {
            registerGetter(store, type, fn)
        })

        // 新建 Vue 實例響應式存儲
        resetStoreVM(this, state)
    }
    get state() {
        return  this._vm._data.$$state
    }
}
// 註冊 getter 函數
function registerGetter (store, type, fn) {
    Object.defineProperty(store.getters, type, {
        get() {
            return fn(store._state)
        }
    })
}

function forEachValue (obj, fn) {
    Object.keys(obj).forEach(key => fn(obj[key], key))
}
複製代碼

利用了 ES5 的 Object.defineProperty 進行攔截,每次調用取值都返回函數運行的結果,

::: tip 也可使用 Proxy 完成攔截,感興趣的同窗能夠本身實現一下 :::

咱們測試圖例的按鈕使用 getters 進行取值,進行到這裏已經能在按鈕上看到這個值了!

getters效果

mutations 和 actions 實現

單純的數據獲取是蒼白的,接下來咱們就來實現數據變化的黑魔法。

由於簡單版本的 mutationsactions 實現大同小異,

因此咱們這裏就放在一塊兒進行實現了。

須要注意的是這裏的 mutation 必須使用 commit 進行調用,這裏使用 _committing 對其加鎖。

class Store {
    constructor(options = {}, Vue) {
        this._committing = false
        ...
    }

    ....

    // 執行函數並加鎖
    _withCommit (fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
    }
}
複製代碼

這裏咱們梳理一下 mutationsactions 的建構流程,

  • 循環配置中的對應函數加載到 Store 的對應位置
  • 定義好 commitdispatch 方法使其指向咱們存儲處理函數的位置
  • 處理好調用時的 this 指向

清晰了流程以後咱們最後的實現代碼就是下面這樣的:

export class Store {
    constructor(options = {}, Vue) {
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        
        const { state = {}, getters = {}, mutations = {}, actions = {} } = options
        
        // 初始化
        this._committing = false
        this._state = state
        this._actions = Object.create(null)
        this._mutations = Object.create(null)
        this.getters = Object.create(null)

        const { dispatch, commit } = this
        const store = this

        // 裝載 getters
        forEachValue(getters, (fn, type) => {
            registerGetter(store, type, fn)
        })

        // 裝載 mutations 和 actions
        forEachValue(mutations, (fn, type) => {
            registerMutation(store, type, fn)
        })

        forEachValue(actions, (fn, type) => {
            registerAction(store, type, fn)
        })

        this.dispatch = function boundDispatch (type, payload) {
            return dispatch.call(store, type, payload)
        }

        this.commit = function boundCommit (type, payload) {
            return commit.call(store, type, payload)
        }
        
        // 新建 Vue 實例響應式存儲
        resetStoreVM(this, state)
    }

    get state() {
        return  this._vm._data.$$state
    }

    // 禁止再賦值
    set state (v) {
        throw new Error('不容許賦值!!!')
    }

    // commit
    commit(type, payload) {
        const entry = this._mutations[type]

        if (!entry) {
            console.error(`[vuex] unknown mutation type: ${type}`)
            return
        }
        // 執行對應處理函數
        this._withCommit(() => {
            entry(payload)
        })
    }

    // dispatch
    dispatch(type, payload) {
        const entry = this._actions[type]

        if (!entry) {
            console.error(`[vuex] unknown action type: ${type}`)
            return
        }
        
        entry (payload)
    }

    // 執行函數並加鎖
    _withCommit (fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
    }

}
複製代碼

看到這裏有沒有長呼一口氣的感受~~

先別鬆懈,咱們還有最後一個問題,

爲了模仿原庫中 Vue.use() 的安裝方式,

咱們還須要提供一個 install 函數

實現入口加載函數

這部分的內容其實就只有兩件事情要作:

  • 取到 Store 實例並將其掛載到 this.$store
  • 將其混入咱們的項目中

這部分的源碼很是好理解,

因此這裏我就直接對源碼進行搬運了~~~

// 安裝方法
export function install (_Vue) {
    if (Vue && _Vue === Vue) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(
          '[vuex] already installed. Vue.use(Vuex) should be called only once.'
        )
      }
      return
    }
    Vue = _Vue
    // 取得 Vue 實例後混入
    Vue.mixin({ beforeCreate: vuexInit })   
}

/** * Vuex init hook, injected into each instances init hooks list. * 初始化 Vuex */
function vuexInit () {
    const options = this.$options
    
    if (options.store) {
      // 組件內部有 store,則優先使用原有的store 
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 組件沒有 store 則繼承根節點的 $store
      this.$store = options.parent.$store
    }
} 
複製代碼

實現效果

實現效果

完整代碼戳這裏

以爲有用的記得 star 一下哦~~

結語

知其然也要知其因此然,

閱讀源碼一方面讓咱們瞭解到框架內部的實現原理,遏制住會產生 bug 的騷操做,

另外一方面也能夠學習精妙的寫法,對本身的編程風格有所啓發。

謝謝大渣!

-- 完 --

歡迎關注個人我的網站啦啦啦~

不按期更新前端內容。

參考資料

相關文章
相關標籤/搜索