本文解讀的Vuex版本為2.3.1
Vuex程式碼結構
Vuex的程式碼並不多,但麻雀雖小,五臟俱全,下面來看一下其中的實現細節。
原始碼分析
入口檔案
入口檔案src/index.js:
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions
}複製程式碼
這是Vuex對外暴露的API,其中核心部分是Store,然後是install,它是一個vue外掛所必須的方法。Store
和install都在store.js檔案中。mapState、mapMutations、mapGetters、mapActions為四個輔助函式,用來將store中的相關屬性對映到元件中。
install方法
Vuejs的外掛都應該有一個install方法。先看下我們通常使用Vuex的姿勢:
import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)複製程式碼
install方法的原始碼:
export 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)
}複製程式碼
方法的入參_Vue就是use的時候傳入的Vue構造器。
install方法很簡單,先判斷下如果Vue已經有值,就丟擲錯誤。這裡的Vue是在程式碼最前面宣告的一個內部變數。
let Vue // bind on install複製程式碼
這是為了保證install方法只執行一次。
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的版本,當vue版本>=2的時候,就在Vue上新增了一個全域性mixin,要麼在init階段,要麼在beforeCreate階段。Vue上新增的全域性mixin會影響到每一個元件。mixin的各種混入方式不同,同名鉤子函式將混合為一個陣列,因此都將被呼叫。並且,混合物件的鉤子將在元件自身鉤子之前。
來看下這個mixin方法vueInit做了些什麼:
this.$options用來獲取例項的初始化選項,當傳入了store的時候,就把這個store掛載到例項的$store上,沒有的話,並且例項有parent的,就把parent的$store掛載到當前例項上。這樣,我們在Vue的元件中就可以通過this.$store.xxx訪問Vuex的各種資料和狀態了。
Store建構函式
Vuex中程式碼最多的就是store.js, 它的建構函式就是Vuex的主體流程。
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 {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}
// store internal state
this._committing = false
this._actions = Object.create(null)
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
// 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.concat(devtoolPlugin).forEach(plugin => plugin(this))
}複製程式碼
依然,先來看看使用Store的通常姿勢,便於我們知道方法的入參:
export default new Vuex.Store({
state,
mutations
actions,
getters,
modules: {
...
},
plugins,
strict: false
})複製程式碼
store建構函式的最開始,進行了2個判斷。
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是util.js裡的一個方法。
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}複製程式碼
先判斷一下Vue是否存在,是為了保證在這之前store已經install過了。另外,Vuex依賴Promise,這裡也進行了判斷。
assert這個函式雖然簡單,但這種程式設計方式值得我們學習。
接著往下看:
const {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}複製程式碼
這裡使用解構並設定預設值的方式來獲取傳入的值,分別得到了plugins, strict 和state。傳入的state也可以是一個方法,方法的返回值作為state。
然後是定義了一些內部變數:
// store internal state
this._committing = false
this._actions = Object.create(null)
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()複製程式碼
this._committing 表示提交狀態,作用是保證對 Vuex 中 state 的修改只能在 mutation 的回撥函式中,而不能在外部隨意修改state。
this._actions 用來存放使用者定義的所有的 actions。
this._mutations 用來存放使用者定義所有的 mutatins。
this._wrappedGetters 用來存放使用者定義的所有 getters。
this._modules 用來儲存使用者定義的所有modules
this._modulesNamespaceMap 存放module和其namespace的對應關係。
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)
}複製程式碼
如同程式碼的註釋一樣,繫結Store類的dispatch和commit方法到當前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, [], 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.concat(devtoolPlugin).forEach(plugin => plugin(this))複製程式碼
Vuex的初始化核心
installModule
使用單一狀態樹,導致應用的所有狀態集中到一個很大的物件。但是,當應用變得很大時,store 物件會變得臃腫不堪。
為了解決以上問題,Vuex 允許我們將 store 分割到模組(module)。每個模組擁有自己的 state、mutation、action、getters、甚至是巢狀子模組——從上至下進行類似的分割。
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)複製程式碼
在進入installModule方法之前,有必要先看下方法的入參this._modules.root是什麼。
this._modules = new ModuleCollection(options)複製程式碼
這裡主要用到了src/module/module-collection.js 和 src/module/module.js
module-collection.js:
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.root = new Module(rawRootModule, false)
// register all nested modules
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
this.register([key], rawModule, false)
})
}
}
...
}複製程式碼
module-collection的建構函式裡先定義了例項的root屬性,為一個Module例項。然後遍歷options裡的modules,依次註冊。
看下這個Module的建構函式:
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
...
}複製程式碼
這裡的rawModule一層一層的傳過來,也就是new Store時候的options。
module例項的_children目前為null,然後設定了例項的_rawModule和state。
回到module-collection建構函式的register方法, 及它用到的相關方法:
register (path, rawModule, runtime = true) {
const parent = this.get(path.slice(0, -1))
const newModule = new Module(rawModule, runtime)
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)
})
}
}
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
addChild (key, module) {
this._children[key] = module
}複製程式碼
get方法的入參path為一個陣列,例如['subModule', 'subsubModule'], 這裡使用reduce方法,一層一層的取值, this.get(path.slice(0, -1))取到當前module的父module。然後再呼叫Module類的addChild方法,將改module新增到父module的_children物件上。
然後,如果rawModule上有傳入modules的話,就遞迴一次註冊。
看下得到的_modules資料結構:
扯了一大圈,就是為了說明installModule函式的入參,接著回到installModule方法。
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)複製程式碼
通過path的length來判斷是不是root module。
來看一下getNamespace這個方法:
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}複製程式碼
又使用reduce方法來累加module的名字。這裡的module.namespaced是定義module的時候的引數,例如:
export default {
state,
getters,
actions,
mutations,
namespaced: true
}複製程式碼
所以像下面這樣定義的store,得到的selectLabelRule的namespace就是'selectLabelRule/'
export default new Vuex.Store({
state,
actions,
getters,
mutations,
modules: {
selectLabelRule
},
strict: debug
})複製程式碼
接著看installModule方法:
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}複製程式碼
傳入了namespaced為true的話,將module根據其namespace放到內部變數_modulesNamespaceMap物件上。
然後
// 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)
})
}複製程式碼
getNestedState跟前面的getNamespace類似,也是用reduce來獲得當前父module的state,最後呼叫Vue.set將state新增到父module的state上。
看下這裡的_withCommit方法:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}複製程式碼
this._committing在Store的建構函式裡宣告過,初始值為false。這裡由於我們是在修改 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 中的狀態,如下圖所示:
來看installModule方法的最後:
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 namespacedType = namespace + key
registerAction(store, namespacedType, action, 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)
})複製程式碼
local為接下來幾個方法的入參,我們又要跑偏去看一下makeLocalContext這個方法了:
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
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 (!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 (!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
}複製程式碼
就像方法的註釋所說的,方法用來得到區域性的dispatch,commit,getters 和 state, 如果沒有namespace的話,就用根store的dispatch, commit等等
以local.dispath為例:
沒有namespace為''的時候,直接使用this.dispatch。有namespace的時候,就在type前加上namespace再dispath。
local引數說完了,接來是分別註冊mutation,action和getter。以註冊mutation為例說明:
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(local.state, payload)
})
}複製程式碼
根據mutation的名字找到內部變數_mutations裡的陣列。然後,將mutation的回到函式push到裡面。
例如有這樣一個mutation:
mutation: {
increment (state, n) {
state.count += n
}
}複製程式碼
就會在_mutations[increment]裡放入其回撥函式。
commit
前面說到mutation被放到了_mutations物件裡。接下來看一下,Store建構函式裡最開始的將Store類的dispatch和commit放到當前例項上,那commit一個mutation的執行情況是什麼呢?
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
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)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state))
if (options && options.silent) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}複製程式碼
方法的最開始用unifyObjectStyle來獲取引數,這是因為commit的傳參方式有兩種:
store.commit('increment', {
amount: 10
})複製程式碼
提交 mutation 的另一種方式是直接使用包含 type 屬性的物件:
store.commit({
type: 'increment',
amount: 10
})複製程式碼
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
return { type, payload, options }
}複製程式碼
如果傳入的是物件,就做引數轉換。
然後判斷需要commit的mutation是否註冊過了,this._mutations[type],沒有就拋錯。
然後迴圈呼叫_mutations裡的每一個mutation回撥函式。
然後執行每一個mutation的subscribe回撥函式。
Vuex輔助函式
Vuex提供的輔助函式有4個:
以mapGetters為例,看下mapGetters的用法:
程式碼在src/helpers.js裡:
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (!(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}複製程式碼
normalizeNamespace方法使用函數語言程式設計的方式,接收一個方法,返回一個方法。
mapGetters接收的引數是一個陣列或者一個物件:
computed: {
// 使用物件展開運算子將 getters 混入 computed 物件中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}複製程式碼
mapGetters({
// 對映 this.doneCount 為 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})複製程式碼
這裡是沒有傳namespace的情況,看下方法的具體實現。
normalizeNamespace開始進行了引數跳轉,傳入的陣列或物件給map,namespace為'' , 然後執行fn(namespace, map)
接著是normalizeMap方法,返回一個陣列,這種形式:
{
key: doneCount,
val: doneTodosCount
}複製程式碼
然後往res物件上塞方法,得到如下形式的物件:
{
doneCount: function() {
return this.$store.getters[doneTodosCount]
}
}複製程式碼
也就是最開始mapGetters想要的效果:
完
by kaola/fangwentian