1. 程式人生 > >Vuex 2.0 原始碼分析

Vuex 2.0 原始碼分析

當我們用 Vue.js 開發一箇中到大型的單頁應用時,經常會遇到如下問題:

https://github.com/DDFE/DDFE-blog/issues/8

  • 如何讓多個 Vue 元件共享狀態
  • Vue 元件間如何通訊

通常,在專案不是很複雜的時候,我們會利用全域性事件匯流排 (global event bus)解決,但是隨著複雜度的提升,這些程式碼將變的難以維護。因此,我們需要一種更加好用的解決方案,於是,Vuex 誕生了。

本文並不是 Vuex 的科普文章,對於還不瞭解 Vuex 的同學,建議先移步 Vuex 官方文件;看英文文件吃力的同學,可以看 Vuex 的中文文件

vuex 原理圖

Vuex 的設計思想受到了 Flux,Redux 和 The Elm Architecture 的啟發,它的實現又十分巧妙,和 Vue.js 配合相得益彰,下面就讓我們一起來看它的實現吧。

目錄結構

Vuex 的原始碼託管在 github,我們首先通過 git 把程式碼 clone 到本地,選一款適合自己的 IDE 開啟原始碼,展開 src 目錄,如下圖所示:

enter image description here

src 目錄下的檔案並不多,包含幾個 js 檔案和 plugins 目錄, plugins 目錄裡面包含 2 個 Vuex 的內建外掛,整個原始碼加起來不過 500-600 行,可謂非常輕巧的一個庫。

麻雀雖小,五臟俱全,我們先直觀的感受一下原始碼的結構,接下來看一下其中的實現細節。

原始碼分析

本文的原始碼分析過程不會是自上而下的給程式碼加註釋,我更傾向於是從 Vuex 提供的 API 和我們的使用方法等維度去分析。Vuex 的原始碼是基於 es6 的語法編寫的,對於不瞭解 es6 的同學,建議還是先學習一下 es6。

從入口開始

看原始碼一般是從入口開始,Vuex 原始碼的入口是 src/index.js,先來開啟這個檔案。

我們首先看這個庫的 export ,在 index.js 程式碼最後。

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

這裡可以一目瞭然地看到 Vuex 對外暴露的 API。其中, Store 是 Vuex 提供的狀態儲存類,通常我們使用 Vuex 就是通過建立 Store 的例項,稍後我們會詳細介紹。接著是 install 方法,這個方法通常是我們編寫第三方 Vue 外掛的“套路”,先來看一下“套路”程式碼:

function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

我們實現了一個 install 方法,這個方法當我們全域性引用 Vue ,也就是 window 上有 Vue 物件的時候,會手動呼叫 install 方法,並傳入 Vue 的引用;當 Vue 通過 npm 安裝到專案中的時候,我們在程式碼中引入第三方 Vue 外掛通常會編寫如下程式碼:

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)

當我們執行 Vue.use(Vuex) 這句程式碼的時候,實際上就是呼叫了 install 的方法並傳入 Vue 的引用。install 方法顧名思義,現在讓我們來看看它的實現。它接受了一個引數 _Vue,函式體首先判斷 Vue ,這個變數的定義在 index.js 檔案的開頭部分:

let Vue // bind on install

對 Vue 的判斷主要是保證 install 方法只執行一次,這裡把 install 方法的引數 _Vue 物件賦值給 Vue 變數,這樣我們就可以在 index.js 檔案的其它地方使用 Vue 這個變量了。install 方法的最後呼叫了 applyMixin 方法,我們順便來看一下這個方法的實現,在 src/mixin.js 檔案裡定義:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

這段程式碼的作用就是在 Vue 的生命週期中的初始化(1.0 版本是 init,2.0 版本是 beforeCreated)鉤子前插入一段 Vuex 初始化程式碼。這裡做的事情很簡單——給 Vue 的例項注入一個 $store 的屬性,這也就是為什麼我們在 Vue 的元件中可以通過 this.$store.xxx 訪問到 Vuex 的各種資料和狀態。

認識 Store 建構函式

我們在使用 Vuex 的時候,通常會例項化 Store 類,然後傳入一個物件,包括我們定義好的 actions、getters、mutations、state等,甚至當我們有多個子模組的時候,我們可以新增一個 modules 物件。那麼例項化的時候,到底做了哪些事情呢?帶著這個疑問,讓我們回到 index.js 檔案,重點看一下 Store 類的定義。Store 類定義的程式碼略長,我不會一下就貼上所有程式碼,我們來拆解分析它,首先看一下建構函式的實現:

class Store {
  constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      state = {},
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._options = options
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], options)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
  ...
}  

建構函式的一開始就用了“斷言函式”,來判斷是否滿足一些條件。

assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)

這行程式碼的目的是確保 Vue 的存在,也就是在我們例項化 Store 之前,必須要保證之前的 install 方法已經執行了。

assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

這行程式碼的目的是為了確保 Promsie 可以使用的,因為 Vuex 的原始碼是依賴 Promise 的。Promise 是 es6 提供新的 API,由於現在的瀏覽器並不是都支援 es6 語法的,所以通常我們會用 babel 編譯我們的程式碼,如果想使用 Promise 這個 特性,我們需要在 package.json 中新增對 babel-polyfill 的依賴並在程式碼的入口加上import 'babel-polyfill' 這段程式碼。

再來看看 assert 這個函式,它並不是瀏覽器原生支援的,它的實現在 src/util.js 裡,程式碼如下:

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

非常簡單,對 condition 判斷,如果不不為真,則丟擲異常。這個函式雖然簡單,但這種程式設計方式值得我們學習。

再來看建構函式接下來的程式碼:

const {
  state = {},
  plugins = [],
  strict = false
} = options

這裡就是利用 es6 的結構賦值拿到 options 裡的 state,plugins 和 strict。state 表示 rootState,plugins 表示應用的外掛、strict 表示是否開啟嚴格模式。

接著往下看:

// store internal state
this._options = options
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._runtimeModules = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()

這裡主要是建立一些內部的屬性:
this._options 儲存引數 options。
this._committing 標誌一個提交狀態,作用是保證對 Vuex 中 state 的修改只能在 mutation 的回撥函式中,而不能在外部隨意修改 state。
this._actions 用來儲存使用者定義的所有的 actions。
this._mutations 用來儲存使用者定義所有的 mutatins。
this._wrappedGetters 用來儲存使用者定義的所有 getters 。
this._runtimeModules 用來儲存所有的執行時的 modules。
this._subscribers 用來儲存所有對 mutation 變化的訂閱者。
this._watcherVM 是一個 Vue 物件的例項,主要是利用 Vue 例項方法 $watch 來觀測變化的。

繼續往下看:

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

// strict mode
this.strict = strict

這裡的程式碼也不難理解,把 Store 類的 dispatch 和 commit 的方法的 this 指標指向當前 store 的例項上,dispatch 和 commit 的實現我們稍後會分析。this.strict 表示是否開啟嚴格模式,在嚴格模式下會觀測所有的 state 的變化,建議在開發環境時開啟嚴格模式,線上環境要關閉嚴格模式,否則會有一定的效能開銷。

Vuex 的初始化核心

installModule

我們接著往下看:

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], options)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

這段程式碼是 Vuex 的初始化的核心,其中,installModule 方法是把我們通過 options 傳入的各種屬性模組註冊和安裝;resetStoreVM 方法是初始化 store._vm,觀測 state 和 getters 的變化;最後是應用傳入的外掛。

下面,我們先來看一下 installModule 的實現:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    state,
    actions,
    mutations,
    getters,
    modules
  } = module

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }

  if (getters) {
    wrapGetters(store, getters, path)
  }

  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}

installModule 函式可接收5個引數,store、rootState、path、module、hot,store 表示當前 Store 例項,rootState 表示根 state,path 表示當前巢狀模組的路徑陣列,module 表示當前安裝的模組,hot 當動態改變 modules 或者熱更新的時候為 true。

先來看這部分程式碼:

 const isRoot = !path.length
 const {
   state,
   actions,
   mutations,
   getters,
   modules
 } = module

程式碼首先通過 path 陣列的長度判斷是否為根。我們在建構函式呼叫的時候是 installModule(this, state, [], options),所以這裡 isRoot 為 true。module 為傳入的 options,我們拿到了 module 下的 state、actions、mutations、getters 以及巢狀的 modules。

接著看下面的程式碼:

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, state || {})
  })
}

這裡判斷當不為根且非熱更新的情況,然後設定級聯狀態,這裡乍一看不好理解,我們先放一放,稍後來回顧。

再往下看程式碼:

if (mutations) {
  Object.keys(mutations).forEach(key => {
    registerMutation(store, key, mutations[key], path)
  })
}

if (actions) {
  Object.keys(actions).forEach(key => {
    registerAction(store, key, actions[key], path)
  })
}

if (getters) {
  wrapGetters(store, getters, path)
}

這裡分別是對 mutations、actions、getters 進行註冊,如果我們例項化 Store 的時候通過 options 傳入這些物件,那麼會分別進行註冊,我稍後再去介紹註冊的具體實現。那麼到這,如果 Vuex 沒有 module ,這個 installModule 方法可以說已經做完了。但是 Vuex 巧妙了設計了 module 這個概念,因為 Vuex 本身是單一狀態樹,應用的所有狀態都包含在一個大物件內,隨著我們應用規模的不斷增長,這個 Store 變得非常臃腫。為了解決這個問題,Vuex 允許我們把 store 分 module(模組)。每一個模組包含各自的 state、mutations、actions 和 getters,甚至是巢狀模組。所以,接下來還有一行程式碼:

if (modules) {
  Object.keys(modules).forEach(key => {
    installModule(store, rootState, path.concat(key), modules[key], hot)
  })
}

這裡通過遍歷 modules,遞迴呼叫 installModule 去安裝子模組。這裡傳入了 store、rootState、path.concat(key)、和 modules[key],和剛才不同的是,path 不為空,module 對應為子模組,那麼我們回到剛才那段程式碼:

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, state || {})
  })
}

當遞迴初始化子模組的時候,isRoot 為 false,注意這裡有個方法getNestedState(rootState, path),來看一下 getNestedState 函式的定義:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    
            
           

相關推薦

Vuex 2.0 原始碼分析(下)

輔助函式 Vuex 除了提供我們 Store 物件外,還對外提供了一系列的輔助函式,方便我們在程式碼中使用 Vuex,提供了操作 store 的各種屬性的一系列語法糖,下面我們來一起看一下: mapState mapState 工具函式會將 store 中的 state 對映到區域性計算

Vuex 2.0 原始碼分析(上)

當我們用 Vue.js 開發一箇中到大型的單頁應用時,經常會遇到如下問題: 如何讓多個 Vue 元件共享狀態 Vue 元件間如何通訊 通常,在專案不是很複雜的時候,我們會利用全域性事件匯流排 (global event bus)解決,但是隨著複雜度的提升,這些程式碼將變的難以

Vuex 2.0 原始碼分析

當我們用 Vue.js 開發一箇中到大型的單頁應用時,經常會遇到如下問題: https://github.com/DDFE/DDFE-blog/issues/8 如何讓多個 Vue 元件共享狀態Vue 元件間如何通訊 通常,在專案不是很複雜的時候

spring boot 2.0 原始碼分析(一)

在學習spring boot 2.0原始碼之前,我們先利用spring initializr快速地建立一個基本的簡單的示例: 1.先從建立示例中的main函式開始讀起: package com.example; import org.springfra

AFNetWorking(3.0)原始碼分析(四)——AFHTTPSessionManager(2)

在上一篇部落格中,我們分析了AFHTTPSessionManager,以及它是如何實現GET/HEAD/PATCH/DELETE相關介面的。 我們還剩下POST相關介面沒有分析,在這篇部落格裡面,我們就來分析一下POST相關介面是如何實現的。 multipart/form-data請

webpack-tapable-0.2.8 原始碼分析

webpack 是基於事件流的打包構建工具,也就是內建了很多 hooks。作為使用方,可以在這些鉤子當中,去插入自己的處理邏輯,而這一切的實現都得益於 tapable 這個工具。它有多個版本,webpack 前期的版本是依賴於 tapable 0.2.8 這個版本,後來重構了,發了 2.0.0 beta 版本

Fabric 1.0原始碼分析(2) blockfile(區塊檔案儲存)

Fabric 1.0原始碼筆記 之 blockfile(區塊檔案儲存) 1、blockfile概述 blockfile,即Fabric區塊鏈區塊檔案儲存,預設目錄/var/hyperledger/production/ledgersData/chains,含in

區塊鏈教程Fabric1.0原始碼分析flogging(Fabric日誌系統)

  區塊鏈教程Fabric1.0原始碼分析flogging(Fabric日誌系統),2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆記 之 flo

區塊鏈教程Fabric1.0原始碼分析流言演算法Gossip服務端二

  區塊鏈教程Fabric1.0原始碼分析流言演算法Gossip服務端二 Fabric 1.0原始碼筆記 之 gossip(流言演算法) #GossipServer(Gossip服務端) 5.2、commImpl結構體方法 //conn.serviceConnection(),啟動連線服務 func (

區塊鏈教程Fabric1.0原始碼分析流言演算法Gossip服務端一

  區塊鏈教程Fabric1.0原始碼分析流言演算法Gossip服務端一,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆記 之 gossip(流

區塊鏈教程Fabric1.0原始碼分析Ledger blkstorage block檔案儲存

  區塊鏈教程Fabric1.0原始碼分析Ledger blkstorage block檔案儲存,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆

區塊鏈教程Fabric1.0原始碼分析Ledger statedb(狀態資料庫)

Fabric 1.0原始碼筆記 之 Ledger #statedb(狀態資料庫) 1、statedb概述 statedb,或VersionedDB,即狀態資料庫,儲存了交易(transaction)日誌中所有鍵的最新值,也稱世界狀態(world state)。可選擇基於leveldb或cauchdb實現。

兄弟連區塊鏈教程Fabric1.0原始碼分析ledgerID資料

1、idStore概述 Fabric支援建立多個Ledger,不同Ledger以ledgerID區分。 多個ledgerID及其創世區塊儲存在idStore資料庫中,idStore資料庫基於leveldb實現。 idStore預設使用路徑:/var/hyperledger/production

區塊鏈教程Fabric1.0原始碼分析MSP成員關係服務提供者一

Fabric 1.0原始碼筆記 之 MSP(成員關係服務提供者) 1、MSP概述 MSP,全稱Membership Service Provider,即成員關係服務提供者,作用為管理Fabric中的眾多參與者。 成員服務提供者(MSP)是一個提供抽象化成員操作框架的元件。MSP將頒發與校驗證書,以及使用

區塊鏈教程Fabric1.0原始碼分析configupdate處理通道配置更新

  區塊鏈教程Fabric1.0原始碼分析configupdate處理通道配置更新,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆記 之 Ord

區塊鏈教程Fabric1.0原始碼分析Orderer multichain

  區塊鏈教程Fabric1.0原始碼分析Orderer multichain,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆記 之 Order

區塊鏈教程Fabric1.0原始碼分析Peer

  區塊鏈教程Fabric1.0原始碼分析Peer,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.0原始碼筆記 之 Peer 1、Peer概述 在Fa

區塊鏈教程Fabric1.0原始碼分析Peer peer channel命令及子命令實現

  區塊鏈教程Fabric1.0原始碼分析Peer peer channel命令及子命令實現,2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric1.0原始碼筆記之P

區塊鏈教程Fabric1.0原始碼分析Peer EndorserClient(Endorser

  區塊鏈教程Fabric1.0原始碼分析Peer EndorserClient(Endorser客戶端),2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric 1.

區塊鏈教程Fabric1.0原始碼分析PeerBroadcastClient(Broadcas

  區塊鏈教程Fabric1.0原始碼分析PeerBroadcastClient(Broadcast客戶端),2018年下半年,區塊鏈行業正逐漸褪去發展之初的浮躁、迴歸理性,表面上看相關人才需求與身價似乎正在回落。但事實上,正是初期泡沫的漸退,讓人們更多的關注點放在了區塊鏈真正的技術之上。 Fabric1.