本文以
vuex v3.0.1
版本進行分析
install
vuex
提供了一個 install
方法,用於給 vue.use
進行註冊,install
方法對 vue
的版本做了一個判斷,1.x
版本和 2.x
版本的外掛註冊方法是不一樣的:
// vuex/src/mixin.js
if (version >= 2) {
Vue.mixin({ 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)
}
}
複製程式碼
對於 1.x
版本,直接將vuexInit
方法混入到 vueInit
方法中,當 vue
初始化的時候,vuex
也就隨之初始化了
而對於 2.x
版本,則是通過混入 mixin
的方式,全域性混入了一個 beforeCreated
鉤子函式
這個 vuexInit
方法如下:
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
複製程式碼
目的很明確,就是把 options.store
儲存在 this.$store
中,options
就是在 new
一個 vue
例項的時候,傳入的引數集合物件,如果想使用 vuex
的話,肯定要把 store
傳進來的,類似於下面的程式碼,所以可以拿到 options.store
,this
指的是當前 Vue
例項,這個options.store
就是Store
物件的例項,所以可以在元件中通過 this.$store
訪問到這個 Store
例項
const app = new Vue({
el: '#app',
// 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
store,
components: { Counter },
template: '<div class="app"></div>'
})
複製程式碼
Store
上面在 beforeCreate
生命週期中會拿到 options.store
,這個 store
自然也有初始化的過程
每一個 Vuex
應用的核心就是 store
,所以需要有 Store
的初始化過程,下面是一個最簡單的 Store
示例(來源於 vuex的官網):
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
複製程式碼
Store
的原始碼位於 vuex/src/store.js
,在這個類的 constructor
中,new
了一個 vue
例項,所以vuex
可以使用 vue
的很多特性,比如資料的響應式邏輯
初始化Store
的過程中,其實也是對 modules
、dispatch
、commit
等進行了初始化操作
初始化module,構建module tree
在 Store
類的初始化函式 constructor
中,下面這句就是 modules
的初始化入口:
this._modules = new ModuleCollection(options)
複製程式碼
ModuleCollection
是一個 ES6
的類
// src/module/module-collection.js
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
複製程式碼
這個類的 constructor
中呼叫了 register
方法,第二個引數 rawRootModule
就是 Store
初始化時傳進來引數物件 options
// src/module/module-collection.js
register (path, rawModule, runtime = true) {
// ...省略無關程式碼
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
複製程式碼
register
方法中會 new Module
,這個 Module
就是用來描述單個模組的類,裡面定義了與單個模組相關(module
)的資料結構(data struct
)、屬性以及方法,大概如下:
這裡面的方法、屬性等,都和後續構建 Module Tree
有關
由於每個 module
都有其自己的 state
、namespaced
、actions
等,所以在初始化 module
的過程中,也會給每個 module
物件上掛載這些屬性或方法,例如,下面就是掛載 state
的程式碼:
// src/module/module.js
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
複製程式碼
開始初始化的時候,符合path.length === 0
,所以執行 this.root = newMudle
,接著遇到了 if (rawModule.modules)
這個判斷語句,前面說了 rawModule
是傳入的 options
,所以這裡的 rawModule.modules
就是類似下面示例程式碼 ExampleA
中的modules
/**
* 示例程式碼 ExampleA
*/
const store = new Vuex.Store({
// ... 省略無關程式碼
modules: {
profile: {
state: { age: 18 },
getters: {
age (state) {
return state.age
}
}
},
account: {
namespaced: true,
state: {
isAdmin: true,
isLogin: true
},
getters: {
isAdmin (state) {
return state.isAdmin
}
},
actions: {
login ({ state, commit, rootState }) {
commit('goLogin')
}
},
mutations: {
goLogin (state) {
state.isLogin = !state.isLogin
}
},
// 巢狀模組
modules: {
// 進一步巢狀名稱空間
myCount: {
namespaced: true,
state: { count: 1 },
getters: {
count (state) {
return state.count
},
countAddOne (state, getters, c, d) {
console.log(123, state, getters, c, d);
return store.getters.count
}
},
actions: {
addCount ({ commit }) {
commit('addMutation')
},
delCount ({ commit }) {
commit('delMutation')
},
changeCount ({ dispatch }, { type } = { type: 1 }) {
if (type === 1) {
dispatch('addCount')
} else {
dispatch('delCount')
}
}
},
mutations: {
addMutation (state) {
console.log('addMutation1');
state.count = state.count + 1
},
delMutation (state) {
state.count = state.count - 1
}
}
},
// 繼承父模組的名稱空間
posts: {
state: { popular: 2 },
getters: {
popular (state) {
return state.popular
}
}
}
}
}
}
})
複製程式碼
所以這個判斷是用於處理使用了 module
的情況,如果存在 modules
,則呼叫 forEachValue
對 modules
這個物件進行遍歷處理
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
複製程式碼
拿到 modules
裡面存在的所有 module
,進行 register
操作,這裡面的 key
就是每個 module
的名稱,例如 示例程式碼 ExampleA
中的 profile
、account
到這裡再次呼叫 this.register
方法的時候,path.length === 0
就不成立了,所以走 else
的邏輯,這裡遇到了一個this.get
方法:
// src/module/module-collection.js
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
複製程式碼
首先對 path
進行遍歷,然後對遍歷到的項呼叫 getChild
,這個getChild
方法是前面 Module
類中的方法,用於根據 key
,也就是在當前模組中根據模組名獲取子模組物件,與之對應的方法是 addChild
,是給當前模組新增一個子模組,也就是建立父子間的關聯關係:
// src/module/module.js
this._children = Object.create(null)
// ...
addChild (key, module) {
this._children[key] = module
}
// ...
getChild (key) {
return this._children[key]
}
複製程式碼
看到這裡應該就有點思路了,上述一系列操作實際上就是為了以模組名作為屬性 key
,遍歷所有模組及其子模組,構成一棵以 this.root
為頂點的 Modules Tree
,畫成流程圖的話會很清晰:
安裝 module tree
上面構建好一棵 module tree
之後,接下來就要 install
這棵樹了
// src/store.js
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
複製程式碼
這個方法裡做了很多事情,一個個看
首先是對名稱空間 namespaced
的處理,如果發現當前 module
具有 namespaced
屬性並且值為 true
,則會將其註冊到 namespace map
,也就是存起來:
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
複製程式碼
其中 getNamespace
方法就是 ModuleCollection
類上的方法,用於根據 path
拼接出當前模組的完整 namespace
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
複製程式碼
呼叫 getNamespace
獲得名稱空間名稱,然後將名稱空間名稱作為 key
,將對應的名稱空間所指的 module
物件作為 value
快取到 store._modulesNamespaceMap
上,方便後續根據 namespace
查詢模組,這個東西是可以通過 this.$store._modulesNamespaceMap
取到的,例如,對於 ExampleA
中的示例:
接下來是一個判斷邏輯,符合 !isRoot && !hot
條件才能執行,這裡的 isRoot
是在 installModule
方法的開頭定義的:
const isRoot = !path.length
複製程式碼
path
就是 module tree
維護的 module
父子關係的狀態,當 path.length !== 0
時,isRoot
就是 true
,其實這裡就是判斷當前安裝的模組是不是 root
模組,也就是 module tree
最頂層的節點,這個節點的 path.length
就是 0
由於 module
的安裝,在 module tree
上就是從父級到子級,一開始執行 installModule
方法時,傳入的 path
為 []
,則path.length === 0
,所以會執行判斷語句裡面的程式碼
設定 state
// src/store.js
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
複製程式碼
呼叫了 getNestedState
:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
複製程式碼
這裡實際上是通過一層層 path.reduce
來查詢最終的子模組的 state
例如,對於 account/myCount
下的 state
來說,它的 path
是 ['account', 'myCount']
,全域性 state
結構如下:
{
profile: { ... },
account: {
isAdmin: true,
isLogin: true,
// 這是子模組 myCount的名稱空間
myCount: {
// 這是子模組myCount的state
count: 1
},
posts: {
popular: 2
}
}
}
複製程式碼
當對這個全域性 state
和 path = ['account', 'myCount']
呼叫 getNestedState
方法時,最終將得到 /myCount
的 state
:
{
count: 1
}
複製程式碼
查詢到具體子模組的 state
後,掛載到 store._withCommit
上,至於為什麼掛到這上,這裡暫且不分析,後面會說到
構建本地上下文
接下來會執行一個 makeLocalContext
方法:
const local = module.context = makeLocalContext(store, namespace, path)
複製程式碼
關於這個方法的作用,在它的註釋上已經大概描述了一遍:
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
function makeLocalContext (store, namespace, path) {
// ...
}
複製程式碼
大概意思就是,本地化 dispatch
、 commit
、 getter
、state
,如果(當前模組)沒有 namespace
,則直接掛載到 root module
上
可能還是不太明白說的是什麼意思,實際上,這就是對名稱空間模組的一個處理,是為了在呼叫相應模組的 dispatch
、commit
、getters
以及 state
的時候,如果模組使用用了名稱空間,則自動在路徑上追加上 namespace
比如,對於 dispath
而言,如果當前模組存在 namespace
,則在呼叫這個模組的 dispatch
方法的時候,把 namespace
拼接到 type
上,然後根據這個拼接之後的 type
來查詢 store
上的方法並執行:
// makeLocalContext
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
type = namespace + type
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
}
複製程式碼
例如,對於 ExampleA
程式碼而言,想要改變 account/myCount
下的 count
值,可以直接全域性呼叫 this.$store.dispatch('account/myCount/changeCount')
, 當 type = 1
的時候又會執行 dispatch('addCount')
,這個 dispatch
其實是想要執行 account/myCount
模組下的 addCount
這個 actions
,而不是 root module
下的 addCount
於是,這裡就進行了一個全路徑 type
的拼接,將當前模組的 namespace
和 type
拼接到一起,即 account/myCount/
與 addCount
的拼接,最後就拼接成了 account/myCount/addCount
,正是我們想要的 path
,最後將這個全路徑 type
作為引數傳給 store.dispatch
方法,這個過程主要是簡化了巢狀 module
路徑的拼接
commit
的邏輯與此類似,不過 getter
和 state
就有點不一樣了
// src/store.js
// makeLocalContext
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
複製程式碼
對於 getter
,如果沒有 namspace
則直接返回 store.getters
,否則就呼叫 makeLocalGetters
:
// src/store.js
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) return
// extract local getter type
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
}
複製程式碼
直接看這段程式碼可能不太清晰,所以這裡帶入一個例子看,比如對於 account/myCount/count
這個 getter
來說(即上述原始碼中的 type
),它的 namespace
就是 account/myCount/
,它的 localType
就是 count
,當訪問 gettersProxy.count
這個 getters
的時候,會自動指向全域性的 account/myCount/count
然後是 state
,呼叫了 getNestedState
,這個方法前面已經說過了,作用和上面的大體一致,就不多說了
另外,這個過程中多次用到 Object.defineProperty
來設定給物件上的屬性設定 get
函式,而不是直接給屬性賦值,例如上面的 localType
,這種做法的目的在程式碼上也已經註釋得很清楚了,就是為了能夠做到在訪問的時候才計算值,既減少了實時運算量,主要是又能夠保證獲取到的值是實時準確的,這個跟 vue
的響應式機制相關,這裡就不多說了
綜上, makeLocalContext
這個方法實際上就是做了一個具有名稱空間的子模組的 dispatch
、commit
、getter
、state
到全域性的對映:
vuex
的官網在介紹 Actions 這一節的時候,有這麼一段話:
其中, Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件
這句話裡的 context
物件指的就是這裡本地化的 module
物件
註冊 mutation action getter
Mutation
首先是 Mutation
:
// src/store.js
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
複製程式碼
這個 forEachMutation
方法是掛在 module
例項上的,這個方法沒什麼好說的,作用就是遍歷當前 module
上的 mutations
,然後將這些 mutation
作為引數傳入 registerMutation
方法中:
// src/store.js
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
複製程式碼
該方法是給 root store
上的 _mutations[types]
新增 wrappedMutationHandler
方法(至於這個方法是幹什麼的是另外的問題,這裡暫且不去看),而且 store._mutations[type]
的值是一個陣列,也就是說同一個type
的 _mutations
是可以對應多個 wrappedMutationHandler
方法的
例如,對於 ExampleA
中的 account/myCount
這個 module
來說,如果它的 namespaced
屬性不存在,或者其值是 false
,即沒有單獨的名稱空間,然後它的 mutations
中又有個叫 goLogin
的方法,這個方法在 account
這個 module
的 mutations
中同樣存在,於是 state._mutations['account/goLogin']
的陣列中就存在了兩項,一個是 account
下的 goLogin
方法,一個是 account/myCount
下的 goLogin
方法
而如果 account/myCount
的 namespaced
為 true
,就不存在這種情況了,因為這個時候,它的goLogin
對應的 type
是 account/myCount/goLogin
action
// src/store.js
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
複製程式碼
邏輯其實和上面的 Mutation
差不多,都是遍歷所有的 actions
,然後掛到 store
的某個屬性上,只不過 action
是掛到 store._actions
上,同樣的,對於同一個 key
,也可以對應多個 action
方法,這也跟名稱空間有關
getter
// src/store.js
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
複製程式碼
getter
和上面的邏輯也都是差不多的,遍歷所有的 getter
,然後掛到 store
的某個屬性上,只不過 getter
是掛到 store._wrappedGetters
上,另外,對於同一個 key
,只允許存在一個值,如果存在多個值,則以第二個為準:
// src/store.js
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
複製程式碼
最後,如果當前模組具有子模組,則遍歷其所有子模組,給這些子模組執行 installModule
方法,也就是把上面的步驟再次走一遍
至此,installModule
方法就執行完了,這裡再回頭整體看一遍, 呼叫 installModule
這個方法的時候,程式碼上面有兩行註釋:
// src/store.js
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
複製程式碼
大概意思就是:
初始化 root module
同時也會遞迴地註冊所有子 module
並且會將所有 module的 getters 收集到 this._wrappedGetters上
複製程式碼
經過上述的分析,再看這段註釋,就沒什麼難以理解的了,這個方法(installModule
)就是用於包括子模組在內的所有模組的state、getters、actions、mutations
的一個初始化工作
初始化 store vm
接下來,又執行了 resetStoreVM
:
// src/store.js
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
複製程式碼
這個方法的作用可以從它的註釋上大概看出來,初始化 store vm
,看到這個 vm
我們應該想到 vue
的例項 vm
,這裡實際上就是讓 store
藉助 vue
的響應式機制
並且會將 _wrappedGetters
註冊為 computed
的屬性,也就是計算屬性,_wrappedGetters
前面已經提到過了,就是各個模組的 getters
的集合,計算屬性在 vue
中的特性之一是 計算屬性是基於它們的依賴進行快取的。只在相關依賴發生改變時它們才會重新求值,也就是做到了 高效地實時計算,這裡就是想讓 store
上各個模組的 getters
也具備這種特性
// src/store.js
// resetStoreVM
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
複製程式碼
使用 forEachValue
來遍歷 _wrappedGetters
,forEachValue
前面也提到過了,所以這裡的 fn(store)
實際上就是:
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
複製程式碼
也就是 wrappedGetter
這個函式,返回一個 rawGetter
方法執行的結果,這裡的 rawGetter
可以看作就是 getter
計算得到的結果,所以我們在 getter
方法的引數中拿到的四個引數指的就是上面四個:
// https://vuex.vuejs.org/zh/api/#getters
state, // 如果在模組中定義則為模組的區域性狀態
getters, // 等同於 store.getters
rootState // 等同於 store.state
rootGetters // 所有 getters
複製程式碼
拿到 getter
之後,就把它交給 computed
接下來又定義了一個 Object.defineProperty
:
// src、store.js
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
複製程式碼
將 store.getters[key]
對映到 store._vm[key]
上,也就是當訪問 store.getters[key]
的時候,就相當於獲取store._vm[key]
的計算值,至於這裡的 store_vm
又是什麼,跟下面的邏輯有關:
// src、store.js
store._vm = new Vue({
data: {
$$state: state
},
computed
})
複製程式碼
store._vm
實際上就是一個 vue
例項,這個例項只有 data
和 computed
屬性,就是為了藉助 vue
的響應式機制
這裡實際上就是建立了一個 state
與 getter
的對映關係,因為 getter
的計算結果肯定依賴於 state
的,它們之間必然存在關聯的關係,Store
類上有個 state
的訪問器屬性:
// src/store.js
get state () {
return this._vm._data.$$state
}
複製程式碼
於是 state
到 getter
的對映關係流程如下:
接下來是一個用於規範開發方式的邏輯:
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
複製程式碼
store.strict
這裡的 strict
是需要開發者在初始化 Store
的時候顯式宣告的,一般似乎大家都不怎麼關心這個,不過為了更好地遵循 vuex
的開發規範,最好還是加上這個屬性
enableStrictMode
方法如下:
// src/store.js
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
複製程式碼
上面說了,store._vm
其實就是一個 vue
例項,所以它有 $watch
方法,用於檢測 this._data.$$state
的變化,也就是 state
的變化,當 state
變化的時候,store._committing
的值必須為 true
這個 store._committing
的值在 Store
的初始化程式碼中就已經定義了,值預設為 false
:
// src/store.js
this._committing = false
複製程式碼
這個值的修改是在 _withCommit
方法中:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
複製程式碼
確保在執行 fn
的時候, this._committing
值為 true
,然後執行完了再重置回去,這個 _withCommit
的執行場景一般都是對 state
進行修改,例如 commit
:
// src/store.js
commit (_type, _payload, _options) {
// 省略無關程式碼
// ...
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 省略無關程式碼
// ...
}
複製程式碼
enableStrictMode
主要就是為了防止不通過 vuex
提供的方法,例如 commit
、replaceState
等,非法修改 state
值的情況,在開發環境下會報警告
總結
從上述分析來看,vuex
的初始化基本上與 store
的初始化緊密相關,store
初始化完畢,vuex
基本上也就初始化好了,不過過程中涉及到的部分還是比較多的
分析到現在,都是在說初始化,vuex
的 api
幾乎沒說上多少,而vuex
的能力就是通過 api
來體現的,有空再分析下 vuex api
相關的吧