Vuex 2.0 原始碼分析
當我們用 Vue.js 開發一箇中到大型的單頁應用時,經常會遇到如下問題:
https://github.com/DDFE/DDFE-blog/issues/8
- 如何讓多個 Vue 元件共享狀態
- Vue 元件間如何通訊
通常,在專案不是很複雜的時候,我們會利用全域性事件匯流排 (global event bus)解決,但是隨著複雜度的提升,這些程式碼將變的難以維護。因此,我們需要一種更加好用的解決方案,於是,Vuex 誕生了。
本文並不是 Vuex 的科普文章,對於還不瞭解 Vuex 的同學,建議先移步 Vuex 官方文件;看英文文件吃力的同學,可以看 Vuex 的中文文件
Vuex 的設計思想受到了 Flux,Redux 和 The Elm Architecture 的啟發,它的實現又十分巧妙,和 Vue.js 配合相得益彰,下面就讓我們一起來看它的實現吧。
目錄結構
Vuex 的原始碼託管在 github,我們首先通過 git 把程式碼 clone 到本地,選一款適合自己的 IDE 開啟原始碼,展開 src 目錄,如下圖所示:
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.