一、Vuex 的理解
-
Vuex
是一個專爲Vue.js
應用程序開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。 -
狀態管理模式,讓我們從一個簡單的
Vue
計數應用開始:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
- 這個狀態自管理應用包含以下幾個部分:
state
,驅動應用的數據源;view
,以聲明方式將state
映射到視圖;actions
,響應在view
上的用戶輸入導致的狀態變化。
- 單向數據流,但是,當我們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:
- 多個視圖依賴於同一狀態。
- 來自不同視圖的行爲需要變更同一狀態。
-
對於問題一,傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能爲力。對於問題二,我們經常會採用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。因此,我們爲什麼不把組件的共享狀態抽取出來,以一個全局單例模式管理呢?在這種模式下,我們的組件樹構成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行爲。
-
Vuex
核心思想,Vuex
應用的核心就是store
(倉庫)。store
基本上就是一個容器,它包含着你的應用中大部分的狀態 (state
)。定義一個全局對象,再去上層封裝了一些數據存取的接口不也可以麼?Vuex
和單純的全局對象有以下兩點不同:
Vuex
的狀態存儲是響應式的。當Vue
組件從store
中讀取狀態的時候,若store
中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。- 你不能直接改變
store
中的狀態。改變store
中的狀態的唯一途徑就是顯式地提交 (commit
)mutation
。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。 - 通過定義和隔離狀態管理中的各種概念並強制遵守一定的規則,我們的代碼將會變得更結構化且易維護。
二、Vuex 初始化
-
分析
Vuex
的初始化過程,它包括安裝、Store
實例化過兩個方面。 -
安裝,當我們在代碼中通過
import Vuex from 'vuex'
的時候,實際上引用的是一個對象,它的定義在src/index.js
中:
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
和 Vue-Router 一樣,Vuex 也同樣存在一個靜態的
install
方法,它的定義在src/store.js
中:
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
install
的邏輯很簡單,把傳入的_Vue
賦值給Vue
並執行了applyMixin(Vue)
方法,它的定義在src/mixin.js
中:
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
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)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
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
}
}
}
-
applyMixin
就是這個export default function
,它還兼容了Vue 1.0
的版本,這裏我們只關注Vue 2.0
以上版本的邏輯,它其實就全局混入了一個beforeCreate
鉤子函數,它的實現非常簡單,就是把options.store
保存在所有組件的this.$store
中,這個options.store
就是我們在實例化Store
對象的實例,這也是爲什麼我們在組件中可以通過this.$store
訪問到這個實例。 -
Store
實例化,我們在import Vuex
之後,會實例化其中的Store
對象,返回store
實例並傳入new Vue
的options
中,也就是我們剛纔提到的options.store
。舉個簡單的例子,如下:
export default new Vuex.Store({
actions,
getters,
state,
mutations,
modules
// ...
})
Store
對象的構造函數接收一個對象參數,它包含actions
、getters
、state
、mutations
、modules
等Vuex
的核心概念,它的定義在src/store.js
中:
export class Store {
constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `Store must be called with the new operator.`)
}
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = 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
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
}
}
我們把
Store
的實例化過程拆成三個部分,分別是初始化模塊,安裝模塊和初始化store._vm
,接下來我們來分析這三部分的實現。
- 初始化模塊,在分析模塊初始化之前,我們先來了解一下模塊對於
Vuex
的意義:由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象,當應用變得非常複雜時,store
對象就有可能變得相當臃腫。爲了解決以上問題,Vuex
允許我們將store
分割成模塊(module
)。每個模塊擁有自己的state
、mutation
、action
、getter
,甚至是嵌套子模塊——從上至下進行同樣方式的分割:
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... },
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
- 從數據結構上來看,模塊的設計就是一個樹型結構,
store
本身可以理解爲一個root module
,它下面的modules
就是子模塊,Vuex
需要完成這顆樹的構建,構建過程的入口就是:
this._modules = new ModuleCollection(options)
ModuleCollection
的定義在src/module/module-collection.js
中:
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
update (rawRootModule) {
update([], this.root, rawRootModule)
}
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
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)
})
}
}
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return
parent.removeChild(key)
}
}
-
ModuleCollection
實例化的過程就是執行了register
方法,
register
接收三個參數,其中path
表示路徑,因爲我們整體目標是要構建一顆模塊樹,path
是在構建樹的過程中維護的路徑;rawModule
表示定義模塊的原始配置;runtime
表示是否是一個運行時創建的模塊。 -
register
方法首先通過const newModule = new Module(rawModule, runtime)
創建了一個Module
的實例,Module
是用來描述單個模塊的類,它的定義在src/module/module.js
中:
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
get namespaced () {
return !!this._rawModule.namespaced
}
addChild (key, module) {
this._children[key] = module
}
removeChild (key) {
delete this._children[key]
}
getChild (key) {
return this._children[key]
}
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
forEachChild (fn) {
forEachValue(this._children, fn)
}
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
- 來看一下
Module
的構造函數,對於每個模塊而言,this._rawModule
表示模塊的配置,this._children
表示它的所有子模塊,this.state
表示這個模塊定義的state
。回到register
,那麼在實例化一個Module
後,判斷當前的path
的長度如果爲0
,則說明它是一個根模塊,所以把newModule
賦值給了this.root
,否則就需要建立父子關係了:
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
- 我們先大體上了解它的邏輯:首先根據路徑獲取到父模塊,然後再調用父模塊的
addChild
方法建立父子關係。register
的最後一步,就是遍歷當前模塊定義中的所有modules
,根據key
作爲path
,遞歸調用register
方法,這樣我們再回過頭看一下建立父子關係的邏輯,首先執行了this.get(path.slice(0, -1)
方法:
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
- 傳入的
path
是它的父模塊的path
,然後從根模塊開始,通過reduce
方法一層層去找到對應的模塊,查找的過程中,執行的是module.getChild(key)
方法:
getChild (key) {
return this._children[key]
}
- 其實就是返回當前模塊的
_children
中對應key
的模塊,那麼每個模塊的_children
是如何添加的呢,是通過執行parent.addChild(path[path.length - 1], newModule)
方法:
addChild (key, module) {
this._children[key] = module
}
-
對於
root module
的下一層modules
來說,它們的parent
就是root module
,那麼他們就會被添加的root module
的_children
中。每個子模塊通過路徑找到它的父模塊,然後通過父模塊的addChild
方法建立父子關係,遞歸執行這樣的過程,最終就建立一顆完整的模塊樹。 -
安裝模塊,初始化模塊後,執行安裝模塊的相關邏輯,它的目標就是對模塊中的
state
、getters
、mutations
、actions
做初始化工作,它的入口代碼是:
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
來看一下
installModule
的定義:
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = 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, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
installModule
方法支持五個參數,store
表示root store
;state
表示root state
;path
表示模塊的訪問路徑;module
表示當前的模塊,hot
表示是否是熱更新。接下來看函數邏輯,這裏涉及到了命名空間的概念,默認情況下,模塊內部的action
、mutation
和getter
是註冊在全局命名空間的——這樣使得多個模塊能夠對同一mutation
或action
作出響應。如果我們希望模塊具有更高的封裝度和複用性,可以通過添加namespaced: true
的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的所有getter
、action
及mutation
都會自動根據模塊註冊的路徑調整命名。例如:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模塊內容(module assets)
state: { ... }, // 模塊內的狀態已經是嵌套的了,使用 `namespaced` 屬性不會對其產生影響
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模塊
modules: {
// 繼承父模塊的命名空間
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 進一步嵌套命名空間
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
回到
installModule
方法,我們首先根據path
獲取namespace
:
const namespace = store._modules.getNamespace(path)
getNamespace
的定義在 src/module/module-collection.js
中:
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
- 從
root module
開始,通過reduce
方法一層層找子模塊,如果發現該模塊配置了namespaced
爲true
,則把該模塊的key
拼到namesapce
中,最終返回完整的namespace
字符串。回到installModule
方法,接下來把namespace
對應的模塊保存下來,爲了方便以後能根據namespace
查找模塊:
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
- 接下來判斷非
root module
且非hot
的情況執行一些邏輯,我們稍後再看。接着是很重要的邏輯,構造了一個本地上下文環境:
const local = module.context = makeLocalContext(store, namespace, path)
來看一下
makeLocalContext
實現:
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
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)
},
commit: noNamespace ? store.commit : (_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._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
makeLocalContext
支持三個參數相關,store
表示root store
;namespace
表示模塊的命名空間,path
表示模塊的path
。該方法定義了local
對象,對於dispatch
和commit
方法,如果沒有namespace
,它們就直接指向了root store
的dispatch
和commit
方法,否則會創建方法,把type
自動拼接上namespace
,然後執行store
上對應的方法。對於getters
而言,如果沒有namespace
,則直接返回root store
的getters
,否則返回makeLocalGetters(store, namespace)
的返回值:
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
}
makeLocalGetters
首先獲取了namespace
的長度,然後遍歷root store
下的所有getters
,先判斷它的類型是否匹配namespace
,只有匹配的時候我們從namespace
的位置截取後面的字符串得到localType
,接着用Object.defineProperty
定義了gettersProxy
,獲取localType
實際上是訪問了store.getters[type]
。回到makeLocalContext
方法,再來看一下對state
的實現,它的獲取則是通過getNestedState(store.state, path)
方法:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
getNestedState
邏輯很簡單,從root state
開始,通過path.reduce
方法一層層查找子模塊state
,最終找到目標模塊的state
。那麼構造完local
上下文後,我們再回到installModule
方法,接下來它就會遍歷模塊中定義的mutations
、actions
、getters
,分別執行它們的註冊工作,它們的註冊邏輯都大同小異,如下所示:
registerMutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
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)
})
}
首先遍歷模塊中的
mutations
的定義,拿到每一個mutation
和key
,並把key
拼接上namespace
,然後執行registerMutation
方法。該方法實際上就是給root store
上的_mutations[types]
添加wrappedMutationHandler
方法,該方法的具體實現我們之後會提到。注意,同一type
的_mutations
可以對應多個方法。
registerAction
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
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
}
})
}
首先遍歷模塊中的
actions
的定義,拿到每一個action
和key
,並判斷action.root
,如果否的情況把key
拼接上namespace
,然後執行registerAction
方法。該方法實際上就是給root store
上的_actions[types]
添加wrappedActionHandler
方法,該方法的具體實現我們之後會提到。注意,同一type
的_actions
可以對應多個方法。
registerGetter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
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
)
}
}
首先遍歷模塊中的
getters
的定義,拿到每一個getter
和key
,並把key
拼接上namespace
,然後執行registerGetter
方法。該方法實際上就是給root store
上的_wrappedGetters[key]
指定wrappedGetter
方法,該方法的具體實現我們之後會提到。注意,同一type
的_wrappedGetters
只能定義一個。
再回到
installModule
方法,最後一步就是遍歷模塊中的所有子modules
,遞歸執行installModule
方法:
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
- 之前忽略了非
root module
下的state
初始化邏輯,現在來看一下:
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
方法,它是從root state
開始,一層層根據模塊名能訪問到對應path
的state
,那麼它每一層關係的建立實際上就是通過這段state
的初始化邏輯。store._withCommit
方法我們之後再介紹。所以installModule
實際上就是完成了模塊下的state
、getters
、actions
、mutations
的初始化工作,並且通過遞歸遍歷的方式,就完成了所有子模塊的安裝工作。 -
初始化
store._vm
,Store
實例化的最後一步,就是執行初始化store._vm
的邏輯,它的入口代碼是:
resetStoreVM(this, state)
來看一下
resetStoreVM
的定義:
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
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
})
})
// 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: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
-
resetStoreVM
的作用實際上是想建立getters
和state
的聯繫,因爲從設計上getters
的獲取就依賴了state
,並且希望它的依賴能被緩存起來,且只有當它的依賴值發生了改變纔會被重新計算。因此這裏利用了Vue
中用computed
計算屬性來實現。 -
resetStoreVM
首先遍歷了_wrappedGetters
獲得每個getter
的函數fn
和key
,然後定義了computed[key] = () => fn(store)
。我們之前提到過_wrappedGetters
的初始化過程,這裏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
)
}
- 返回的就是
rawGetter
的執行函數,rawGetter
就是用戶定義的getter
函數,它的前兩個參數是local state
和local getters
,後兩個參數是root state
和root getters
。接着實例化一個Vue
實例store._vm
,並把computed
傳入:
store._vm = new Vue({
data: {
$$state: state
},
computed
})
我們發現
data
選項裏定義了$$state
屬性,而我們訪問store.state
的時候,實際上會訪問Store
類上定義的state
的get
方法:
get state () {
return this._vm._data.$$state
}
它實際上就訪問了
store._vm._data.$$state
。那麼getters
和state
是如何建立依賴邏輯的呢,我們再看這段代碼邏輯:
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
})
})
- 當我根據
key
訪問store.getters
的某一個getter
的時候,實際上就是訪問了store._vm[key]
,也就是computed[key]
,在執行computed[key]
對應的函數的時候,會執行rawGetter(local.state,...)
方法,那麼就會訪問到store.state
,進而訪問到store._vm._data.$$state
,這樣就建立了一個依賴關係。當store.state
發生變化的時候,下一次再訪問store.getters
的時候會重新計算。我們再來看一下strict mode
的邏輯:
if (store.strict) {
enableStrictMode(store)
}
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
會添加一個wathcer
來觀測this._data.$$state
的變化,也就是當store.state
被修改的時候,store._committing
必須爲true
,否則在開發階段會報警告。store._committing
默認值是false
,那麼它什麼時候會true
呢,Store
定義了_withCommit
實例方法:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
它就是對
fn
包裝了一個環境,確保在fn
中執行任何邏輯的時候this._committing = true
。所以外部任何非通過 Vuex 提供的接口直接操作修改state
的行爲都會在開發階段觸發警告。
- 總結:
Vuex
的初始化過程就分析完畢了,除了安裝部分,我們重點分析了Store
的實例化過程。我們要把store
想象成一個數據倉庫,爲了更方便的管理倉庫,我們把一個大的store
拆成一些modules
,整個modules
是一個樹型結構。每個module
又分別定義了state
,getters
,mutations
、actions
,我們也通過遞歸遍歷模塊的方式都完成了它們的初始化。爲了module
具有更高的封裝度和複用性,還定義了namespace
的概念。最後我們還定義了一個內部的Vue
實例,用來建立state
到getters
的聯繫,並且可以在嚴格模式下監測state
的變化是不是來自外部,確保改變state
的唯一途徑就是顯式地提交mutation
。我們已經建立好store
,接下來就是對外提供了一些API
方便我們對這個store
做數據存取的操作,後面我們就來從源碼角度來分析Vuex
提供的一系列API
。