本文解讀的Vuex版本爲2.3.1javascript
Vuex的代碼並很少,但麻雀雖小,五臟俱全,下面來看一下其中的實現細節。vue
入口文件src/index.js:java
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中的相關屬性映射到組件中。react
Vuejs的插件都應該有一個install方法。先看下咱們一般使用Vuex的姿式: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的各類數據和狀態了。app
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))複製代碼
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]裏放入其回調函數。
前面說到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提供的輔助函數有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