做者:滴滴公共前端團隊 - 黃軼javascript
你們好,我叫黃軼,來自滴滴公共前端團隊,咱們團隊最近寫了一本書 ——《Vue.js 權威指南》,內容豐富,由淺入深。不過有一些同窗反饋說缺乏 Vuex 的介紹的章節。既然 Vue.js 2.0 已經正式發佈了,咱們也要緊跟步伐,和你們聊一聊 Vuex 2.0。本文並不打算講官網已有的內容,而會經過源碼分析的方式,讓同窗們從另一個角度認識和理解 Vuex 2.0。html
當咱們用 Vue.js 開發一箇中到大型的單頁應用時,常常會遇到以下問題:前端
一般,在項目不是很複雜的時候,咱們會利用全局事件總線 (global event bus)解決,可是隨着複雜度的提高,這些代碼將變的難以維護。所以,咱們須要一種更加好用的解決方案,因而,Vuex 誕生了。vue
本文並非 Vuex 的科普文章,對於還不瞭解 Vuex 的同窗,建議先移步 Vuex 官方文檔;看英文文檔吃力的同窗,能夠看 Vuex 的中文文檔。java
Vuex 的設計思想受到了 Flux,Redux 和 The Elm Architecture 的啓發,它的實現又十分巧妙,和 Vue.js 配合相得益彰,下面就讓咱們一塊兒來看它的實現吧。react
Vuex 的源碼託管在 github,咱們首先經過 git 把代碼 clone 到本地,選一款適合本身的 IDE 打開源碼,展開 src 目錄,以下圖所示:git
src 目錄下的文件並很少,包含幾個 js 文件和 plugins 目錄, plugins 目錄裏面包含 2 個 Vuex 的內置插件,整個源碼加起來不過 500-600 行,可謂很是輕巧的一個庫。es6
麻雀雖小,五臟俱全,咱們先直觀的感覺一下源碼的結構,接下來看一下其中的實現細節。github
本文的源碼分析過程不會是自上而下的給代碼加註釋,我更傾向因而從 Vuex 提供的 API 和咱們的使用方法等維度去分析。Vuex 的源碼是基於 es6 的語法編寫的,對於不瞭解 es6 的同窗,建議仍是先學習一下 es6。vuex
看源碼通常是從入口開始,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 的各類數據和狀態。
咱們在使用 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 的變化,建議在開發環境時開啓嚴格模式,線上環境要關閉嚴格模式,不然會有必定的性能開銷。
咱們接着往下看:
// 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)
: state
}複製代碼
這個方法很簡單,就是根據 path 查找 state 上的嵌套 state。在這裏就是傳入 rootState 和 path,計算出當前模塊的父模塊的 state,因爲模塊的 path 是根據模塊的名稱 concat 鏈接的,因此 path 的最後一個元素就是當前模塊的模塊名,最後調用
store._withCommit(() => {
Vue.set(parentState, moduleName, state || {})
})複製代碼
把當前模塊的 state 添加到 parentState 中。
這裏注意一下咱們用了 store._withCommit 方法,來看一下這個方法的定義:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}複製代碼
因爲咱們是在修改 state,Vuex 中全部對 state 的修改都會用 _withCommit
函數包裝,保證在同步修改 state 的過程當中 this._committing
的值始終爲true。這樣當咱們觀測 state 的變化時,若是 this._committing 的值不爲 true,則能檢查到這個狀態修改是有問題的。
看到這裏,有些同窗可能會有點困惑,舉個例子來直觀感覺一下,以 Vuex 源碼中的 example/shopping-cart 爲例,打開 store/index.js,有這麼一段代碼:
export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})複製代碼
這裏有兩個子 module,cart 和 products,咱們打開 store/modules/cart.js,看一下 cart 模塊中的 state 定義,代碼以下:
const state = {
added: [],
checkoutStatus: null
}複製代碼
咱們運行這個項目,打開瀏覽器,利用 Vue 的調試工具來看一下 Vuex 中的狀態,以下圖所示:
能夠看到,在 rootState 下,分別有 cart 和 products 2個屬性,key 根據模塊名稱而來,value 就是在每一個模塊文件中定義的 state,這就把模塊 state 掛載到 rootState 上了。
咱們瞭解完嵌套模塊 state 是怎麼一回過後,咱們回過頭來看一下 installModule 過程當中的其它 3 個重要方法:registerMutation、registerAction 和 wrapGetters。顧名思義,這 3 個方法分別處理 mutations、actions 和 getters。咱們先來看一下 registerMutation 的定義:
function registerMutation (store, type, handler, path = []) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(getNestedState(store.state, path), payload)
})
}複製代碼
registerMutation 是對 store 的 mutation 的初始化,它接受 4 個參數,store爲當前 Store 實例,type爲 mutation 的 key,handler 爲 mutation 執行的回調函數,path 爲當前模塊的路徑。mutation 的做用就是同步修改當前模塊的 state ,函數首先經過 type 拿到對應的 mutation 對象數組, 而後把一個 mutation 的包裝函數 push 到這個數組中,這個函數接收一個參數 payload,這個就是咱們在定義 mutation 的時候接收的額外參數。這個函數執行的時候會調用 mutation 的回調函數,並經過 getNestedState(store.state, path) 方法獲得當前模塊的 state,和 playload 一塊兒做爲回調函數的參數。舉個例子:
// ...
mutations: {
increment (state, n) {
state.count += n
}
}複製代碼
這裏咱們定義了一個 mutation,經過剛纔的 registerMutation 方法,咱們註冊了這個 mutation,這裏的 state 對應的就是當前模塊的 state,n 就是額外參數 payload,接下來咱們會從源碼分析的角度來介紹這個 mutation 的回調是什麼時候被調用的,參數是如何傳遞的。
咱們有必要知道 mutation 的回調函數的調用時機,在 Vuex 中,mutation 的調用是經過 store 實例的 API 接口 commit 來調用的,來看一下 commit 函數的定義:
commit (type, payload, options) {
// check object-style commit
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
console.error(`[vuex] unknown mutation type: ${type}`)
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
if (!options || !options.silent) {
this._subscribers.forEach(sub => sub(mutation, this.state))
}
}複製代碼
commit 支持 3 個參數,type 表示 mutation 的類型,payload 表示額外的參數,options 表示一些配置,好比 silent 等,稍後會用到。commit 函數首先對 type 的類型作了判斷,處理了 type 爲 object 的狀況,接着根據 type 去查找對應的 mutation,若是找不到,則輸出一條錯誤信息,不然遍歷這個 type 對應的 mutation 對象數組,執行 handler(payload) 方法,這個方法就是以前定義的 wrappedMutationHandler(handler),執行它就至關於執行了 registerMutation 註冊的回調函數,並把當前模塊的 state 和 額外參數 payload 做爲參數傳入。注意這裏咱們依然使用了 this._withCommit
的方法提交 mutation。commit 函數的最後,判斷若是不是靜默模式,則遍歷 this._subscribers
,調用回調函數,並把 mutation 和當前的根 state 做爲參數傳入。那麼這個 this._subscribers
是什麼呢?原來 Vuex 的 Store 實例提供了 subscribe API 接口,它的做用是訂閱(註冊監聽) store 的 mutation。先來看一下它的實現:
subscribe (fn) {
const subs = this._subscribers
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}複製代碼
subscribe 方法很簡單,他接受的參數是一個回調函數,會把這個回調函數保存到 this._subscribers
上,並返回一個函數,當咱們調用這個返回的函數,就能夠解除當前函數對 store 的 mutation 的監聽。其實,Vuex 的內置 logger 插件就是基於 subscribe 接口實現對 store 的 muation的監聽,稍後咱們會詳細介紹這個插件。
在瞭解完 registerMutation,咱們再來看一下 registerAction 的定義:
function registerAction (store, type, handler, path = []) {
const entry = store._actions[type] || (store._actions[type] = [])
const { dispatch, commit } = store
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler({
dispatch,
commit,
getters: store.getters,
state: getNestedState(store.state, path),
rootState: store.state
}, payload, cb)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}複製代碼
registerAction 是對 store 的 action 的初始化,它和 registerMutation 的參數一致,和 mutation 不一樣一點,mutation 是同步修改當前模塊的 state,而 action 是能夠異步去修改 state,這裏不要誤會,在 action 的回調中並不會直接修改 state ,仍然是經過提交一個 mutation 去修改 state(在 Vuex 中,mutation 是修改 state 的惟一途徑)。那咱們就來看看 action 是如何作到這一點的。
函數首先也是經過 type 拿到對應 action 的對象數組,而後把一個 action 的包裝函數 push 到這個數組中,這個函數接收 2 個參數,payload 表示額外參數 ,cb 表示回調函數(實際上咱們並無使用它)。這個函數執行的時候會調用 action 的回調函數,傳入一個 context 對象,這個對象包括了 store 的 commit 和 dispatch 方法、getter、當前模塊的 state 和 rootState 等等。接着對這個函數的返回值作判斷,若是不是一個 Promise 對象,則調用 Promise.resolve(res)
給res 包裝成了一個 Promise 對象。這裏也就解釋了爲什麼 Vuex 的源碼依賴 Promise,這裏對 Promise 的判斷也和簡單,參考代碼 src/util.js,對 isPromise 的判斷以下:
export function isPromise (val) {
return val && typeof val.then === 'function'
}複製代碼
其實就是簡單的檢查對象的 then 方法,若是包含說明就是一個 Promise 對象。
接着判斷 store._devtoolHook
,這個只有當用到 Vuex devtools 開啓的時候,咱們才能捕獲 promise 的過程當中的 。 action 的包裝函數最後返回 res ,它就是一個地地道道的 Promise 對象。來看個例子:
actions: {
checkout ({ commit, state }, payload) {
// 把當前購物車的商品備份起來
const savedCartItems = [...state.cart.added]
// 發送結賬請求,並愉快地清空購物車
commit(types.CHECKOUT_REQUEST)
// 購物 API 接收一個成功回調和一個失敗回調
shop.buyProducts(
products,
// 成功操做
() => commit(types.CHECKOUT_SUCCESS),
// 失敗操做
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}複製代碼
這裏咱們定義了一個 action,經過剛纔的 registerAction 方法,咱們註冊了這個 action,這裏的 commit 就是 store 的 API 接口,能夠經過它在 action 裏提交一個 mutation。state 對應的就是當前模塊的 state,咱們在這個 action 裏便可以同步提交 mutation,也能夠異步提交。接下來咱們會從源碼分析的角度來介紹這個 action 的回調是什麼時候被調用的,參數是如何傳遞的。
咱們有必要知道 action 的回調函數的調用時機,在 Vuex 中,action 的調用是經過 store 實例的 API 接口 dispatch 來調用的,來看一下 dispatch 函數的定義:
dispatch (type, payload) {
// check object-style dispatch
if (isObject(type) && type.type) {
payload = type
type = type.type
}
const entry = this._actions[type]
if (!entry) {
console.error(`[vuex] unknown action type: ${type}`)
return
}
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}複製代碼
dispatch 支持2個參數,type 表示 action 的類型,payload 表示額外的參數。前面幾行代碼和 commit 接口很是相似,都是找到對應 type 下的 action 對象數組,惟一和 commit 不一樣的地方是最後部分,它對 action 的對象數組長度作判斷,若是長度爲 1 則直接調用 entry[0](payload)
, 這個方法就是以前定義的 wrappedActionHandler(payload, cb),執行它就至關於執行了 registerAction 註冊的回調函數,並把當前模塊的 context 和 額外參數 payload 做爲參數傳入。因此咱們在 action 的回調函數裏,能夠拿到當前模塊的上下文包括 store 的 commit 和 dispatch 方法、getter、當前模塊的 state 和 rootState,可見 action 是很是靈活的。
瞭解完 registerAction 後,咱們來看看 wrapGetters的定義:
function wrapGetters (store, moduleGetters, modulePath) {
Object.keys(moduleGetters).forEach(getterKey => {
const rawGetter = moduleGetters[getterKey]
if (store._wrappedGetters[getterKey]) {
console.error(`[vuex] duplicate getter key: ${getterKey}`)
return
}
store._wrappedGetters[getterKey] = function wrappedGetter (store) {
return rawGetter(
getNestedState(store.state, modulePath), // local state
store.getters, // getters
store.state // root state
)
}
})
}複製代碼
wrapGetters 是對 store 的 getters 初始化,它接受 3個 參數, store 表示當前 Store 實例,moduleGetters 表示當前模塊下的全部 getters, modulePath 對應模塊的路徑。細心的同窗會發現,和剛纔的 registerMutation 以及 registerAction 不一樣,這裏對 getters 的循環遍歷是放在了函數體內,而且 getters 和它們的一個區別是不容許 getter 的 key 有重複。
這個函數作的事情就是遍歷 moduleGetters,把每個 getter 包裝成一個方法,添加到 store._wrappedGetters 對象中,注意 getter 的 key 是不容許重複的。在這個包裝的方法裏,會執行 getter 的回調函數,並把當前模塊的 state,store 的 getters 和 store 的 rootState 做爲它參數。來看一個例子:
export const cartProducts = state => {
return state.cart.added.map(({ id, quantity }) => {
const product = state.products.all.find(p => p.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
}複製代碼
這裏咱們定義了一個 getter,經過剛纔的 wrapGetters 方法,咱們把這個 getter 添加到 store._wrappedGetters
對象裏,這和回調函數的參數 state 對應的就是當前模塊的 state,接下來咱們從源碼的角度分析這個函數是如何被調用,參數是如何傳遞的。
咱們有必要知道 getter 的回調函數的調用時機,在 Vuex 中,咱們知道當咱們在組件中經過 this.$store.getters.xxxgetters
能夠訪問到對應的 getter 的回調函數,那麼咱們須要把對應 getter 的包裝函數的執行結果綁定到 `this.$store
上。這部分的邏輯就在 resetStoreVM 函數裏。咱們在 Store 的構造函數中,在執行完 installModule 方法後,就會執行 resetStoreVM 方法。來看一下它的定義:
function resetStoreVM (store, state) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
Object.keys(wrappedGetters).forEach(key => {
const fn = wrappedGetters[key]
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: { state },
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation.
store._withCommit(() => {
oldVm.state = null
})
Vue.nextTick(() => oldVm.$destroy())
}
}複製代碼
這個方法主要是重置一個私有的 _vm 對象,它是一個 Vue 的實例。這個 _vm 對象會保留咱們的 state 樹,以及用計算屬性的方式存儲了 store 的 getters。來具體看看它的實現過程。咱們把這個函數拆成幾個部分來分析:
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
Object.keys(wrappedGetters).forEach(key => {
const fn = wrappedGetters[key]
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})複製代碼
這部分留了現有的 store._vm 對象,接着遍歷 store._wrappedGetters
對象,在遍歷過程當中,依次拿到每一個 getter 的包裝函數,並把這個包裝函數執行的結果用 computed 臨時變量保存。接着用 es5 的 Object.defineProperty 方法爲 store.getters 定義了 get 方法,也就是當咱們在組件中調用this.$store.getters.xxxgetters
這個方法的時候,會訪問 store._vm[xxxgetters]
。咱們接着往下看:
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: { state },
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}複製代碼
這部分的代碼首先先拿全局 Vue.config.silent 的配置,而後臨時把這個配置設成 true,接着實例化一個 Vue 的實例,把 store 的狀態樹 state 做爲 data 傳入,把咱們剛纔的臨時變量 computed 做爲計算屬性傳入。而後再把以前的 silent 配置重置。設置 silent 爲 true 的目的是爲了取消這個 _vm 的全部日誌和警告。把 computed 對象做爲 _vm 的 computed 屬性,這樣就完成了 getters 的註冊。由於當咱們在組件中訪問 this.$store.getters.xxxgetters
的時候,就至關於訪問 store._vm[xxxgetters]
,也就是在訪問 computed[xxxgetters],這樣就訪問到了 xxxgetters 對應的回調函數了。這段代碼最後判斷 strict 屬性決定是否開啓嚴格模式,咱們來看看嚴格模式都幹了什麼:
function enableStrictMode (store) {
store._vm.$watch('state', () => {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}複製代碼
嚴格模式作的事情很簡單,監測 store._vm.state
的變化,看看 state 的變化是否經過執行 mutation 的回調函數改變,若是是外部直接修改 state,那麼 store._committing
的值爲 false,這樣就拋出一條錯誤。再次強調一下,Vuex 中對 state 的修改只能在 mutation 的回調函數裏。
回到 resetStoreVM 函數,咱們來看一下最後一部分:
if (oldVm) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation.
store._withCommit(() => {
oldVm.state = null
})
Vue.nextTick(() => oldVm.$destroy())
}複製代碼
這裏的邏輯很簡單,因爲這個函數每次都會建立新的 Vue 實例並賦值到 store._vm
上,那麼舊的 _vm 對象的狀態設置爲 null,並調用 $destroy 方法銷燬這個舊的 _vm 對象。
那麼到這裏,Vuex 的初始化基本告一段落了,初始化核心就是 installModule 和
resetStoreVM 函數。經過對 mutations 、actions 和 getters 的註冊,咱們瞭解到 state 的是按模塊劃分的,按模塊的嵌套造成一顆狀態樹。而 actions、mutations 和 getters 的全局的,其中 actions 和 mutations 的 key 容許重複,但 getters 的 key 是不容許重複的。官方推薦咱們給這些全局的對象在定義的時候加一個名稱空間來避免命名衝突。
從源碼的角度介紹完 Vuex 的初始化的玩法,咱們再從 Vuex 提供的 API 方向來分析其中的源碼,看看這些 API 是如何實現的。
Vuex 常見的 API 如 dispatch、commit 、subscribe 咱們前面已經介紹過了,這裏就再也不贅述了,下面介紹的一些 Store 的 API,雖然不經常使用,可是瞭解一下也不錯。
watch 做用是響應式的監測一個 getter 方法的返回值,當值改變時調用回調。getter 接收 store 的 state 做爲惟一參數。來看一下它的實現:
watch (getter, cb, options) {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
return this._watcherVM.$watch(() => getter(this.state), cb, options)
}複製代碼
函數首先斷言 watch 的 getter 必須是一個方法,接着利用了內部一個 Vue 的實例對象 `this._watcherVM
的 $watch 方法,觀測 getter 方法返回值的變化,若是有變化則調用 cb 函數,回調函數的參數爲新值和舊值。watch 方法返回的是一個方法,調用它則取消觀測。
registerModule 的做用是註冊一個動態模塊,有的時候當咱們異步加載一些業務的時候,能夠經過這個 API 接口去動態註冊模塊,來看一下它的實現:
registerModule (path, module) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
this._runtimeModules[path.join('.')] = module
installModule(this, this.state, path, module)
// reset store to update getters...
resetStoreVM(this, this.state)
}複製代碼
函數首先對 path 判斷,若是 path 是一個 string 則把 path 轉換成一個 Array。接着把 module 對象緩存到 this._runtimeModules
這個對象裏,path 用點鏈接做爲該對象的 key。接着和初始化 Store 的邏輯同樣,調用 installModule 和 resetStoreVm 方法安裝一遍動態注入的 module。
和 registerModule 方法相對的就是 unregisterModule 方法,它的做用是註銷一個動態模塊,來看一下它的實現:
unregisterModule (path) {
if (typeof path === 'string') path = [path]
assert(Array.isArray(path), `module path must be a string or an Array.`)
delete this._runtimeModules[path.join('.')]
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}複製代碼
函數首先仍是對 path 的類型作了判斷,這部分邏輯和註冊是同樣的。接着從 this._runtimeModules
裏刪掉以 path 點鏈接的 key 對應的模塊。接着經過 this._withCommit
方法把當前模塊的 state 對象從父 state 上刪除。最後調用 resetStore(this) 方法,來看一下這個方法的定義:
function resetStore (store) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
const state = store.state
// init root module
installModule(store, state, [], store._options, true)
// init all runtime modules
Object.keys(store._runtimeModules).forEach(key => {
installModule(store, state, key.split('.'), store._runtimeModules[key], true)
})
// reset vm
resetStoreVM(store, state)
}複製代碼
這個方法做用就是重置 store 對象,重置 store 的 _actions、_mutations、_wrappedGetters
等等屬性。而後再次調用 installModules 去從新安裝一遍 Module 對應的這些屬性,注意這裏咱們的最後一個參數 hot 爲true,表示它是一次熱更新。這樣在 installModule 這個方法體類,以下這段邏輯就不會執行
function installModule (store, rootState, path, module, hot) {
...
// 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 || {})
})
}
...
}複製代碼
因爲 hot 始終爲 true,這裏咱們就不會從新對狀態樹作設置,咱們的 state 保持不變。由於咱們已經明確的刪除了對應 path 下的 state 了,要作的事情只不過就是從新註冊一遍 muations、actions 以及 getters。
回調 resetStore 方法,接下來遍歷 this._runtimeModules
模塊,從新安裝全部剩餘的 runtime Moudles。最後仍是調用 resetStoreVM 方法去重置 Store 的 _vm 對象。
hotUpdate 的做用是熱加載新的 action 和 mutation。 來看一下它的實現:
hotUpdate (newOptions) {
updateModule(this._options, newOptions)
resetStore(this)
}複製代碼
函數首先調用 updateModule 方法去更新狀態,其中當前 Store 的 opition 配置和要更新的 newOptions 會做爲參數。來看一下這個函數的實現:
function updateModule (targetModule, newModule) {
if (newModule.actions) {
targetModule.actions = newModule.actions
}
if (newModule.mutations) {
targetModule.mutations = newModule.mutations
}
if (newModule.getters) {
targetModule.getters = newModule.getters
}
if (newModule.modules) {
for (const key in newModule.modules) {
if (!(targetModule.modules && targetModule.modules[key])) {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
return
}
updateModule(targetModule.modules[key], newModule.modules[key])
}
}
}複製代碼
首先咱們對 newOptions 對象的 actions、mutations 以及 getters 作了判斷,若是有這些屬性的話則替換 targetModule(當前 Store 的 options)對應的屬性。最後判斷若是 newOptions 包含 modules 這個 key,則遍歷這個 modules 對象,若是 modules 對應的 key 不在以前的 modules 中,則報一條警告,由於這是添加一個新的 module ,須要手動從新加載。若是 key 在以前的 modules,則遞歸調用 updateModule,熱更新子模塊。
調用完 updateModule 後,回到 hotUpdate 函數,接着調用 resetStore 方法從新設置 store,剛剛咱們已經介紹過了。
replaceState的做用是替換整個 rootState,通常在用於調試,來看一下它的實現:
replaceState (state) {
this._withCommit(() => {
this._vm.state = state
})
}複製代碼
函數很是簡單,就是調用 this._withCommit
方法修改 Store 的 rootState,之因此提供這個 API 是因爲在咱們是不能在 muations 的回調函數外部去改變 state。
到此爲止,API 部分介紹完了,其實整個 Vuex 源碼下的 src/index.js 文件裏的代碼基本都過了一遍。
Vuex 除了提供咱們 Store 對象外,還對外提供了一系列的輔助函數,方便咱們在代碼中使用 Vuex,提供了操做 store 的各類屬性的一系列語法糖,下面咱們來一塊兒看一下:
mapState 工具函數會將 store 中的 state 映射到局部計算屬性中。爲了更好理解它的實現,先來看一下它的使用示例:
// vuex 提供了獨立的構建工具函數 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函數可讓代碼很是簡潔
count: state => state.count,
// 傳入字符串 'count' 等同於 `state => state.count`
countAlias: 'count',
// 想訪問局部狀態,就必須藉助於一個普通函數,函數中使用 `this` 獲取局部狀態
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}複製代碼
當計算屬性名稱和狀態子樹名稱對應相同時,咱們能夠向 mapState 工具函數傳入一個字符串數組。
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])複製代碼
經過例子咱們能夠直觀的看到,mapState 函數能夠接受一個對象,也能夠接收一個數組,那它底層到底幹了什麼事呢,咱們一塊兒來看一下源碼這個函數的定義:
export function mapState (states) {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
return typeof val === 'function'
? val.call(this, this.$store.state, this.$store.getters)
: this.$store.state[val]
}
})
return res
}複製代碼
函數首先對傳入的參數調用 normalizeMap 方法,咱們來看一下這個函數的定義:
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}複製代碼
這個方法判斷參數 map 是否爲數組,若是是數組,則調用數組的 map 方法,把數組的每一個元素轉換成一個 {key, val: key}
的對象;不然傳入的 map 就是一個對象(從 mapState 的使用場景來看,傳入的參數不是數組就是對象),咱們調用 Object.keys 方法遍歷這個 map 對象的 key,把數組的每一個 key 都轉換成一個 {key, val: key}
的對象。最後咱們把這個對象數組做爲 normalizeMap 的返回值。
回到 mapState 函數,在調用了 normalizeMap 函數後,把傳入的 states 轉換成由 {key, val} 對象構成的數組,接着調用 forEach 方法遍歷這個數組,構造一個新的對象,這個新對象每一個元素都返回一個新的函數 mappedState,函數對 val 的類型判斷,若是 val 是一個函數,則直接調用這個 val 函數,把當前 store 上的 state 和 getters 做爲參數,返回值做爲 mappedState 的返回值;不然直接把 this.$store.state[val]
做爲 mappedState 的返回值。
那麼爲什麼 mapState 函數的返回值是這樣一個對象呢,由於 mapState 的做用是把全局的 state 和 getters 映射到當前組件的 computed 計算屬性中,咱們知道在 Vue 中 每一個計算屬性都是一個函數。
爲了更加直觀地說明,回到剛纔的例子:
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函數可讓代碼很是簡潔
count: state => state.count,
// 傳入字符串 'count' 等同於 `state => state.count`
countAlias: 'count',
// 想訪問局部狀態,就必須藉助於一個普通函數,函數中使用 `this` 獲取局部狀態
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}複製代碼
通過 mapState 函數調用後的結果,以下所示:
import { mapState } from 'vuex'
export default {
// ...
computed: {
count() {
return this.$store.state.count
},
countAlias() {
return this.$store.state['count']
},
countPlusLocalState() {
return this.$store.state.count + this.localCount
}
}
}複製代碼
咱們再看一下 mapState 參數爲數組的例子:
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])複製代碼
通過 mapState 函數調用後的結果,以下所示:
computed: {
count() {
return this.$store.state['count']
}
}複製代碼
mapGetters 工具函數會將 store 中的 getter 映射到局部計算屬性中。它的功能和 mapState 很是相似,咱們來直接看它的實現:
export function mapGetters (getters) {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
res[key] = function mappedGetter () {
if (!(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
}
return this.$store.getters[val]
}
})
return res
}複製代碼
mapGetters 的實現也和 mapState 很相似,不一樣的是它的 val 不能是函數,只能是一個字符串,並且會檢查 val in this.$store.getters
的值,若是爲 false 會輸出一條錯誤日誌。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用對象擴展操做符把 getter 混入到 computed 中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}複製代碼
通過 mapGetters 函數調用後的結果,以下所示:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
doneTodosCount() {
return this.$store.getters['doneTodosCount']
},
anotherGetter() {
return this.$store.getters['anotherGetter']
}
}
}複製代碼
再看一個參數 mapGetters 參數是對象的例子:
computed: mapGetters({
// 映射 this.doneCount 到 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})複製代碼
通過 mapGetters 函數調用後的結果,以下所示:
computed: {
doneCount() {
return this.$store.getters['doneTodosCount']
}
}複製代碼
mapActions 工具函數會將 store 中的 dispatch 方法映射到組件的 methods 中。和 mapState、mapGetters 也相似,只不過它映射的地方不是計算屬性,而是組件的 methods 對象上。咱們來直接看它的實現:
export function mapActions (actions) {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
return this.$store.dispatch.apply(this.$store, [val].concat(args))
}
})
return res
}複製代碼
能夠看到,函數的實現套路和 mapState、mapGetters 差很少,甚至更簡單一些, 實際上就是作了一層函數包裝。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment' // 映射 this.increment() 到 this.$store.dispatch('increment')
]),
...mapActions({
add: 'increment' // 映射 this.add() to this.$store.dispatch('increment')
})
}
}複製代碼
通過 mapActions 函數調用後的結果,以下所示:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
increment(...args) {
return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
}
add(...args) {
return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
}
}
}複製代碼
mapMutations 工具函數會將 store 中的 commit 方法映射到組件的 methods 中。和 mapActions 的功能幾乎同樣,咱們來直接看它的實現:
export function mapMutations (mutations) {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
return this.$store.commit.apply(this.$store, [val].concat(args))
}
})
return res
}複製代碼
函數的實現幾乎也和 mapActions 同樣,惟一差異就是映射的是 store 的 commit 方法。爲了更直觀地理解,咱們來看一個簡單的例子:
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment' // 映射 this.increment() 到 this.$store.commit('increment')
]),
...mapMutations({
add: 'increment' // 映射 this.add() 到 this.$store.commit('increment')
})
}
}複製代碼
通過 mapMutations 函數調用後的結果,以下所示:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
increment(...args) {
return this.$store.commit.apply(this.$store, ['increment'].concat(args))
}
add(...args) {
return this.$store.commit.apply(this.$store, ['increment'].concat(args))
}
}
}複製代碼
Vuex 的 store 接收 plugins 選項,一個 Vuex 的插件就是一個簡單的方法,接收 store 做爲惟一參數。插件做用一般是用來監聽每次 mutation 的變化,來作一些事情。
在 store 的構造函數的最後,咱們經過以下代碼調用插件:
import devtoolPlugin from './plugins/devtool'
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))複製代碼
咱們一般實例化 store 的時候,還會調用 logger 插件,代碼以下:
import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
...
plugins: debug ? [createLogger()] : []
})複製代碼
在上述 2 個例子中,咱們分別調用了 devtoolPlugin 和 createLogger() 2 個插件,它們是 Vuex 內置插件,咱們接下來分別看一下他們的實現。
devtoolPlugin 主要功能是利用 Vue 的開發者工具和 Vuex 作配合,經過開發者工具的面板展現 Vuex 的狀態。它的源碼在 src/plugins/devtool.js 中,來看一下這個插件到底作了哪些事情。
const devtoolHook =
typeof window !== 'undefined' &&
window.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
if (!devtoolHook) return
store._devtoolHook = devtoolHook
devtoolHook.emit('vuex:init', store)
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}複製代碼
咱們直接從對外暴露的 devtoolPlugin 函數看起,函數首先判斷了devtoolHook 的值,若是咱們瀏覽器裝了 Vue 開發者工具,那麼在 window 上就會有一個 __VUE_DEVTOOLS_GLOBAL_HOOK__
的引用, 那麼這個 devtoolHook 就指向這個引用。
接下來經過 devtoolHook.emit('vuex:init', store)
派發一個 Vuex 初始化的事件,這樣開發者工具就能拿到當前這個 store 實例。
接下來經過 devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) })
監聽 Vuex 的 traval-to-state 的事件,把當前的狀態樹替換成目標狀態樹,這個功能也是利用 Vue 開發者工具替換 Vuex 的狀態。
最後經過 store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) })
方法訂閱 store 的 state 的變化,當 store 的 mutation 提交了 state 的變化, 會觸發回調函數——經過 devtoolHook 派發一個 Vuex mutation 的事件,mutation 和 rootState 做爲參數,這樣開發者工具就能夠觀測到 Vuex state 的實時變化,在面板上展現最新的狀態樹。
一般在開發環境中,咱們但願實時把 mutation 的動做以及 store 的 state 的變化實時輸出,那麼咱們能夠用 loggerPlugin 幫咱們作這個事情。它的源碼在 src/plugins/logger.js 中,來看一下這個插件到底作了哪些事情。
// Credits: borrowed code from fcomb/redux-logger
import { deepCopy } from '../util'
export default function createLogger ({ collapsed = true, transformer = state => state, mutationTransformer = mut => mut } = {}) {
return store => {
let prevState = deepCopy(store.state)
store.subscribe((mutation, state) => {
if (typeof console === 'undefined') {
return
}
const nextState = deepCopy(state)
const time = new Date()
const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
const formattedMutation = mutationTransformer(mutation)
const message = `mutation ${mutation.type}${formattedTime}`
const startMessage = collapsed
? console.groupCollapsed
: console.group
// render
try {
startMessage.call(console, message)
} catch (e) {
console.log(message)
}
console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
try {
console.groupEnd()
} catch (e) {
console.log('—— log end ——')
}
prevState = nextState
})
}
}
function repeat (str, times) {
return (new Array(times + 1)).join(str)
}
function pad (num, maxLength) {
return repeat('0', maxLength - num.toString().length) + num
}複製代碼
插件對外暴露的是 createLogger 方法,它實際上接受 3 個參數,它們都有默認值,一般咱們用默認值就能夠。createLogger 的返回的是一個函數,當我執行 logger 插件的時候,實際上執行的是這個函數,下面來看一下這個函數作了哪些事情。
函數首先執行了 let prevState = deepCopy(store.state)
深拷貝當前 store 的 rootState。這裏爲何要深拷貝,由於若是是單純的引用,那麼 store.state 的任何變化都會影響這個引用,這樣就沒法記錄上一個狀態了。咱們來了解一下 deepCopy 的實現,在 src/util.js 裏定義:
function find (list, f) {
return list.filter(f)[0]
}
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}複製代碼
deepCopy 並不陌生,不少開源庫如 loadash、jQuery 都有相似的實現,原理也不難理解,主要是構造一個新的對象,遍歷原對象或者數組,遞歸調用 deepCopy。不過這裏的實現有一個有意思的地方,在每次執行 deepCopy 的時候,會用 cache 數組緩存當前嵌套的對象,以及執行 deepCopy 返回的 copy。若是在 deepCopy 的過程當中經過 find(cache, c => c.original === obj)
發現有循環引用的時候,直接返回 cache 中對應的 copy,這樣就避免了無限循環的狀況。
回到 loggerPlugin 函數,經過 deepCopy 拷貝了當前 state 的副本並用 prevState 變量保存,接下來調用 store.subscribe 方法訂閱 store 的 state 的變。 在回調函數中,也是先經過 deepCopy 方法拿到當前的 state 的副本,並用 nextState 變量保存。接下來獲取當前格式化時間已經格式化的 mutation 變化的字符串,而後利用 console.group 以及 console.log 分組輸出 prevState、mutation以及 nextState,這裏能夠經過咱們 createLogger 的參數 collapsed、transformer 以及 mutationTransformer 來控制咱們最終 log 的顯示效果。在函數的最後,咱們把 nextState 賦值給 prevState,便於下一次 mutation。
Vuex 2.0 的源碼分析到這就告一段落了,最後我再分享一下看源碼的當心得:對於一個庫或者框架源碼的研究前,首先了解他們的使用場景、官網文檔等;而後必定要用他,至少也要寫幾個小 demo,達到熟練掌握的程度;最後再從入口、API、使用方法等等多個維度去了解他內部的實現細節。若是這個庫過於龐大,那就先按模塊和功能拆分,一點點地消化。
最後還有一個問題,有些同窗會問,源碼那麼枯燥,咱們分析學習它的有什麼好處呢?首先,學習源碼有助於咱們更深刻掌握和應用這個庫或者框架;其次,咱們還能夠學習到源碼中不少編程技巧,能夠遷移到咱們平時的開發工做中;最後,對於一些高級開發工程師而言,咱們能夠學習到它的設計思想,對未來有一天咱們也去設計一個庫或者框架是很是有幫助的,這也是提高自身能力水平的很是好的途徑。
歡迎關注DDFE
GITHUB:github.com/DDFE
微信公衆號:微信搜索公衆號「DDFE」或掃描下面的二維碼