Vuex 2.0 源碼分析
一、前言
當我們用 Vue.js 開發一個中到大型的單頁應用時,經常會遇到如下問題:
- 如何讓多個 Vue 組件共享狀態
- Vue 組件間如何通訊
通常,在項目不是很復雜的時候,我們會利用全局事件總線 (global event bus)解決,但是隨著復雜度的提升,這些代碼將變的難以維護。因此,我們需要一種更加好用的解決方案,于是,Vuex 誕生了。
Vuex 的設計思想受到了 Flux,Redux 和 The Elm Architecture 的啟發,它的實現又十分巧妙,和 Vue.js 配合相得益彰,下面就讓我們一起來看它的實現吧。
二、目錄結構
Vuex 的源碼托管在 GitHub ,我們首先把代碼 clone 到本地,選一款適合自己的 IDE 打開源碼,展開 src 目錄,如下圖所示:
src 目錄下的文件并不多,包含幾個 js 文件和 plugins 目錄, plugins 目錄里面包含 2 個 Vuex 的內置插件,整個源碼加起來不過 500-600 行,可謂非常輕巧的一個庫。
麻雀雖小,五臟俱全,我們先直觀的感受一下源碼的結構,接下來看一下其中的實現細節。
三、源碼分析
本文的源碼分析過程不會是自上而下的給代碼加注釋,我更傾向于是從 Vuex 提供的 API 和我們的使用方法等維度去分析。Vuex 的源碼是基于 ES6 的語法編寫的,對于不了解 ES6 的同學,建議還是先學習一下 ES6。
1. 從入口開始
看源碼一般是從入口開始,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
}
}
}</code></pre>
這段代碼的作用就是在 Vue 的生命周期中的初始化(1.0 版本是 init,2.0 版本是 beforeCreated)鉤子前插入一段 Vuex 初始化代碼。這里做的事情很簡單——給 Vue 的實例注入一個 $store 的屬性,這也就是為什么我們在 Vue 的組件中可以通過 this.$store.xxx 訪問到 Vuex 的各種數據和狀態。
2. 認識 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
<div class="md-section-divider"></div>
這里的代碼也不難理解,把 Store 類的 dispatch 和 commit 的方法的 this 指針指向當前 store 的實例上,dispatch 和 commit 的實現我們稍后會分析。this.strict 表示是否開啟嚴格模式,在嚴格模式下會觀測所有的 state 的變化,建議在開發環境時開啟嚴格模式,線上環境要關閉嚴格模式,否則會有一定的性能開銷。
3. Vuex 的初始化核心
(1)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))
<div class="md-section-divider"></div>
這段代碼是 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)
})
}
}
<div class="md-section-divider"></div>
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
<div class="md-section-divider"></div>
代碼首先通過 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 || {})
})
}
<div class="md-section-divider"></div>
這里判斷當不為根且非熱更新的情況,然后設置級聯狀態,這里乍一看不好理解,我們先放一放,稍后來回顧。
再往下看代碼:
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)
}
<div class="md-section-divider"></div>
這里分別是對 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)
})
}
<div class="md-section-divider"></div>
這里通過遍歷 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 || {})
})
}
<div class="md-section-divider"></div>
當遞歸初始化子模塊的時候,isRoot 為 false,注意這里有個方法 getNestedState(rootState, path) ,來看一下 getNestedState 函數的定義:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
<div class="md-section-divider"></div>
這個方法很簡單,就是根據 path 查找 state 上的嵌套 state。在這里就是傳入 rootState 和 path,計算出當前模塊的父模塊的 state,由于模塊的 path 是根據模塊的名稱 concat 連接的,所以 path 的最后一個元素就是當前模塊的模塊名,最后調用:
store._withCommit(() => {
Vue.set(parentState, moduleName, state || {})
})
<div class="md-section-divider"></div>
把當前模塊的 state 添加到 parentState 中。
這里注意一下我們用了 store._withCommit 方法,來看一下這個方法的定義:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
<div class="md-section-divider"></div>
由于我們是在修改 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()] : []
})
<div class="md-section-divider"></div>
這里有兩個子 module,cart 和 products,我們打開 store/modules/cart.js,看一下 cart 模塊中的 state 定義,代碼如下:
added: [],
checkoutStatus: null
}
<div class="md-section-divider"></div>
我們運行這個項目,打開瀏覽器,利用 Vue 的調試工具來看一下 Vuex 中的狀態,如下圖所示:

可以看到,在 rootState 下,分別有 cart 和 products 2個屬性,key 根據模塊名稱而來,value 就是在每個模塊文件中定義的 state,這就把模塊 state 掛載到 rootState 上了。
我們了解完嵌套模塊 state 是怎么一回事后,我們回過頭來看一下 installModule 過程中的其它 3 個重要方法:registerMutation、registerAction 和 wrapGetters。顧名思義,這 3 個方法分別處理 mutations、actions 和 getters。我們先來看一下 registerMutation 的定義:
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)
})
}
<div class="md-section-divider"></div>
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
}
}
<div class="md-section-divider"></div>
這里我們定義了一個 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))
}
}
<div class="md-section-divider"></div>
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)
}
}
}
<div class="md-section-divider"></div>
subscribe 方法很簡單,他接受的參數是一個回調函數,會把這個回調函數保存到 this._subscribers 上,并返回一個函數,當我們調用這個返回的函數,就可以解除當前函數對 store 的 mutation 的監聽。其實,Vuex 的內置 logger 插件就是基于 subscribe 接口實現對 store 的 muation的監聽,稍后我們會詳細介紹這個插件。
registerAction
在了解完 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
}
})
}
<div class="md-section-divider"></div>
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'
}
<div class="md-section-divider"></div>
其實就是簡單的檢查對象的 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)
)
}
}
<div class="md-section-divider"></div>
這里我們定義了一個 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)
}
<div class="md-section-divider"></div>
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 是非常靈活的。
wrapGetters
了解完 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
)
}
})
}
<div class="md-section-divider"></div>
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
}
})
}
<div class="md-section-divider"></div>
這里我們定義了一個 getter,通過剛才的 wrapGetters 方法,我們把這個 getter 添加到 store._wrappedGetters 對象里,這和回調函數的參數 state 對應的就是當前模塊的 state,接下來我們從源碼的角度分析這個函數是如何被調用,參數是如何傳遞的。
我們有必要知道 getter 的回調函數的調用時機,在 Vuex 中,我們知道當我們在組件中通過 this.$store.getters.xxxgetters 可以訪問到對應的 getter 的回調函數,那么我們需要把對應 getter 的包裝函數的執行結果綁定到 this.$store 上。這部分的邏輯就在 resetStoreVM 函數里。我們在 Store 的構造函數中,在執行完 installModule 方法后,就會執行 resetStoreVM 方法。來看一下它的定義:
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())
}
}
<div class="md-section-divider"></div>
這個方法主要是重置一個私有的 _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]
})
})
<div class="md-section-divider"></div>
這部分留了現有的 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)
}
<div class="md-section-divider"></div>
這部分的代碼首先先拿全局 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 })
}
<div class="md-section-divider"></div>
嚴格模式做的事情很簡單,監測 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())
}
<div class="md-section-divider"></div>
這里的邏輯很簡單,由于這個函數每次都會創建新的 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 是如何實現的。
4. Vuex API 分析
Vuex 常見的 API 如 dispatch、commit 、subscribe 我們前面已經介紹過了,這里就不再贅述了,下面介紹的一些 Store 的 API,雖然不常用,但是了解一下也不錯。
watch(getter, cb, options)
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)
}
<div class="md-section-divider"></div>
函數首先斷言 watch 的 getter 必須是一個方法,接著利用了內部一個 Vue 的實例對象 this._watcherVM 的 $watch 方法,觀測 getter 方法返回值的變化,如果有變化則調用 cb 函數,回調函數的參數為新值和舊值。watch 方法返回的是一個方法,調用它則取消觀測。
registerModule(path, module)
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)
}
<div class="md-section-divider"></div>
函數首先對 path 判斷,如果 path 是一個 string 則把 path 轉換成一個 Array。接著把 module 對象緩存到 this._runtimeModules 這個對象里,path 用點連接作為該對象的 key。接著和初始化 Store 的邏輯一樣,調用 installModule 和 resetStoreVm 方法安裝一遍動態注入的 module。
unregisterModule(path)
和 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)
}
<div class="md-section-divider"></div>
函數首先還是對 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)
}
<div class="md-section-divider"></div>
這個方法作用就是重置 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 || {})
})
}
...
}
<div class="md-section-divider"></div>
由于 hot 始終為 true,這里我們就不會重新對狀態樹做設置,我們的 state 保持不變。因為我們已經明確的刪除了對應 path 下的 state 了,要做的事情只不過就是重新注冊一遍 muations、actions 以及 getters。
回調 resetStore 方法,接下來遍歷 this._runtimeModules 模塊,重新安裝所有剩余的 runtime Moudles。最后還是調用 resetStoreVM 方法去重置 Store 的 _vm 對象。
hotUpdate(newOptions)
hotUpdate 的作用是熱加載新的 action 和 mutation。 來看一下它的實現:
hotUpdate (newOptions) {
updateModule(this._options, newOptions)
resetStore(this)
}
<div class="md-section-divider"></div>
函數首先調用 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])
}
}
}
<div class="md-section-divider"></div></code></pre>
首先我們對 newOptions 對象的 actions、mutations 以及 getters 做了判斷,如果有這些屬性的話則替換 targetModule(當前 Store 的 options)對應的屬性。最后判斷如果 newOptions 包含 modules 這個 key,則遍歷這個 modules 對象,如果 modules 對應的 key 不在之前的 modules 中,則報一條警告,因為這是添加一個新的 module ,需要手動重新加載。如果 key 在之前的 modules,則遞歸調用 updateModule,熱更新子模塊。
調用完 updateModule 后,回到 hotUpdate 函數,接著調用 resetStore 方法重新設置 store,剛剛我們已經介紹過了。
replaceState
replaceState的作用是替換整個 rootState,一般在用于調試,來看一下它的實現:
replaceState (state) {
this._withCommit(() => {
this._vm.state = state
})
}
<div class="md-section-divider"></div>
函數非常簡單,就是調用 this._withCommit 方法修改 Store 的 rootState,之所以提供這個 API 是由于在我們是不能在 muations 的回調函數外部去改變 state。
到此為止,API 部分介紹完了,其實整個 Vuex 源碼下的 src/index.js 文件里的代碼基本都過了一遍。
5. 輔助函數
Vuex 除了提供我們 Store 對象外,還對外提供了一系列的輔助函數,方便我們在代碼中使用 Vuex,提供了操作 store 的各種屬性的一系列語法糖,下面我們來一起看一下:
mapState
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
}
})
}
<div class="md-section-divider"></div>
當計算屬性名稱和狀態子樹名稱對應相同時,我們可以向 mapState 工具函數傳入一個字符串數組。
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])
<div class="md-section-divider"></div>
通過例子我們可以直觀的看到,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
}
<div class="md-section-divider"></div>
函數首先對傳入的參數調用 normalizeMap 方法,我們來看一下這個函數的定義:
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
<div class="md-section-divider"></div>
這個方法判斷參數 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
}
})
}
<div class="md-section-divider"></div>
經過 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
}
}
}
<div class="md-section-divider"></div>
我們再看一下 mapState 參數為數組的例子:
computed: mapState([
// 映射 this.count 到 this.$store.state.count
'count'
])
<div class="md-section-divider"></div>
經過 mapState 函數調用后的結果,如下所示:
computed: {
count() {
return this.$store.state['count']
}
}
<div class="md-section-divider"></div>
mapGetters
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
}
<div class="md-section-divider"></div>
mapGetters 的實現也和 mapState 很類似,不同的是它的 val 不能是函數,只能是一個字符串,而且會檢查 val in this.$store.getters 的值,如果為 false 會輸出一條錯誤日志。為了更直觀地理解,我們來看一個簡單的例子:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用對象擴展操作符把 getter 混入到 computed 中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
<div class="md-section-divider"></div>
經過 mapGetters 函數調用后的結果,如下所示:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
doneTodosCount() {
return this.$store.getters['doneTodosCount']
},
anotherGetter() {
return this.$store.getters['anotherGetter']
}
}
}
<div class="md-section-divider"></div>
再看一個參數 mapGetters 參數是對象的例子:
computed: mapGetters({
// 映射 this.doneCount 到 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
<div class="md-section-divider"></div>
經過 mapGetters 函數調用后的結果,如下所示:
computed: {
doneCount() {
return this.$store.getters['doneTodosCount']
}
}
<div class="md-section-divider"></div>
mapActions
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
}
<div class="md-section-divider"></div>
可以看到,函數的實現套路和 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')
})
}
}
<div class="md-section-divider"></div>
經過 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))
}
}
}
<div class="md-section-divider"></div>
mapMutations
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
}
<div class="md-section-divider"></div>
函數的實現幾乎也和 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')
})
}
}
<div class="md-section-divider"></div>
經過 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))
}
}
}
<div class="md-section-divider"></div>
6. 插件
Vuex 的 store 接收 plugins 選項,一個 Vuex 的插件就是一個簡單的方法,接收 store 作為唯一參數。插件作用通常是用來監聽每次 mutation 的變化,來做一些事情。
在 store 的構造函數的最后,我們通過如下代碼調用插件:
import devtoolPlugin from './plugins/devtool'
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
<div class="md-section-divider"></div>
我們通常實例化 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()] : []
})
<div class="md-section-divider"></div>
在上述 2 個例子中,我們分別調用了 devtoolPlugin 和 createLogger() 2 個插件,它們是 Vuex 內置插件,我們接下來分別看一下他們的實現。
devtoolPlugin
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)
})
}
<div class="md-section-divider"></div>
我們直接從對外暴露的 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)
})</code></pre>
監聽 Vuex 的 traval-to-state 的事件,把當前的狀態樹替換成目標狀態樹,這個功能也是利用 Vue 開發者工具替換 Vuex 的狀態。
最后通過
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})</code></pre>
方法訂閱 store 的 state 的變化,當 store 的 mutation 提交了 state 的變化, 會觸發回調函數——通過 devtoolHook 派發一個 Vuex mutation 的事件,mutation 和 rootState 作為參數,這樣開發者工具就可以觀測到 Vuex state 的實時變化,在面板上展示最新的狀態樹。
loggerPlugin
通常在開發環境中,我們希望實時把 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
}
<div class="md-section-divider"></div>
插件對外暴露的是 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、使用方法等等多個維度去了解它內部的實現細節。如果這個庫過于龐大,那就先按模塊和功能拆分,一點點地消化。
最后還有一個問題,有些同學會問,源碼那么枯燥,我們分析學習它的有什么好處呢?首先,學習源碼有助于我們更深入掌握和應用這個庫或者框架;其次,我們還可以學習到源碼中很多編程技巧,可以遷移到我們平時的開發工作中;最后,對于一些高級開發工程師而言,我們可以學習到它的設計思想,對將來有一天我們也去設計一個庫或者框架是非常有幫助的,這也是提升自身能力水平的非常好的途徑。
來自:http://www.infoq.com/cn/articles/source-code-vuex2