Vuex是專爲Vue開發的統一狀態管理工具。當咱們的項目不是很複雜時,一些交互能夠經過全局事件總線解決,可是這種觀察者模式有些弊端,開發時可能沒什麼感受,可是當項目變得複雜,維護時每每會摸不着頭腦,若是是後來加入的夥伴更會以爲很無奈。這時候能夠採用Vuex方案,它可使得咱們的項目的數據流變得更加清晰。本文將會分析Vuex的整個實現思路,當是本身讀完源碼的一個總結。vue
先從一個簡單的示例入手,一步一步分析整個代碼的執行過程,下面是官方提供的簡單示例react
// store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
Vue官方建議的插件使用方法是使用Vue.use方法,這個方法會調用插件的install方法,看看install方法都作了些什麼,從index.js中能夠看到install方法在store.js中拋出,部分代碼以下webpack
let Vue // bind on install 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) }
聲明瞭一個Vue變量,這個變量在install方法中會被賦值,這樣能夠給當前做用域提供Vue,這樣作的好處是不須要額外import Vue from 'vue'
不過咱們也能夠這樣寫,而後讓打包工具不要將其打包,而是指向開發者所提供的Vue,好比webpack的externals
,這裏就不展開了。執行install會先判斷Vue是否已經被賦值,避免二次安裝。而後調用applyMixin
方法,代碼以下git
// applyMixin 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 // 當咱們在執行new Vue的時候,須要提供store字段 if (options.store) { // 若是是root,將store綁到this.$store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if (options.parent && options.parent.$store) { // 不然拿parent上的$store // 從而實現全部組件共用一個store實例 this.$store = options.parent.$store } } }
這裏會區分vue的版本,2.x和1.x的鉤子是不同的,若是是2.x使用beforeCreate
,1.x即便用_init
。當咱們在執行new Vue
啓動一個Vue應用程序時,須要給上store
字段,根組件從這裏拿到store
,子組件從父組件拿到,這樣一層一層傳遞下去,實現全部組件都有$store
屬性,這樣咱們就能夠在任何組件中經過this.$store
訪問到store
github
接下去繼續看例子web
// store.js export default new Vuex.Store({ state: { count: 0 }, getters: { evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd' }, actions: { increment: ({ commit }) => commit('increment'), decrement: ({ commit }) => commit('decrement') }, mutations: { increment (state) { state.count++ }, decrement (state) { state.count-- } } })
// app.js new Vue({ el: '#app', store, // 傳入store,在beforeCreate鉤子中會用到 render: h => h(Counter) })
這裏是調用Store構造函數,傳入一個對象,包括state、actions等等,接下去看看Store構造函數都作了些什麼ajax
export class Store { constructor (options = {}) { if (!Vue && typeof window !== 'undefined' && window.Vue) { // 掛載在window上的自動安裝,也就是經過script標籤引入時不須要手動調用Vue.use(Vuex) install(window.Vue) } if (process.env.NODE_ENV !== 'production') { // 斷言必須使用Vue.use(Vuex),在install方法中會給Vue賦值 // 斷言必須存在Promise // 斷言必須使用new操做符 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) // 這裏進行module收集,只處理了state this._modules = new ModuleCollection(options) // 用於保存namespaced的模塊 this._modulesNamespaceMap = Object.create(null) // 用於監聽mutation this._subscribers = [] // 用於響應式地監測一個 getter 方法的返回值 this._watcherVM = new Vue() // 將dispatch和commit方法的this指針綁定到store上,防止被修改 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 // 這裏是module處理的核心,包括處理根module、action、mutation、getters和遞歸註冊子module installModule(this, state, [], this._modules.root) // 使用vue實例來保存state和getter resetStoreVM(this, state) // 插件註冊 plugins.forEach(plugin => plugin(this)) if (Vue.config.devtools) { devtoolPlugin(this) } } }
首先會判斷Vue是否是掛載在window
上,若是是的話,自動調用install
方法,而後進行斷言,必須先調用Vue.use(Vuex)。必須提供Promise
,這裏應該是爲了讓Vuex的體積更小,讓開發者自行提供Promise
的polyfill
,通常咱們可使用babel-runtime
或者babel-polyfill
引入。最後斷言必須使用new操做符調用Store函數。vuex
接下去是一些內部變量的初始化_committing
提交狀態的標誌,在_withCommit中,當使用mutation
時,會先賦值爲true
,再執行mutation
,修改state
後再賦值爲false
,在這個過程當中,會用watch
監聽state的變化時是否_committing
爲true,從而保證只能經過mutation
來修改state
_actions
用於保存全部action,裏面會先包裝一次_actionSubscribers
用於保存訂閱action的回調_mutations
用於保存全部的mutation,裏面會先包裝一次_wrappedGetters
用於保存包裝後的getter_modules
用於保存一棵module樹_modulesNamespaceMap
用於保存namespaced的模塊chrome
接下去的重點是api
this._modules = new ModuleCollection(options)
接下去看看ModuleCollection函數都作了什麼,部分代碼以下
// module-collection.js export default class ModuleCollection { constructor (rawRootModule) { // 註冊 root module (Vuex.Store options) this.register([], rawRootModule, false) } get (path) { // 根據path獲取module return path.reduce((module, key) => { return module.getChild(key) }, this.root) } /* * 遞歸註冊module path是路徑 如 * { * modules: { * a: { * state: {} * } * } * } * a模塊的path => ['a'] * 根模塊的path => [] */ register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== 'production') { // 斷言 rawModule中的getters、actions、mutations必須爲指定的類型 assertRawModule(path, rawModule) } // 實例化一個module const newModule = new Module(rawModule, runtime) if (path.length === 0) { // 根module 綁定到root屬性上 this.root = newModule } else { // 子module 添加其父module的_children屬性上 const parent = this.get(path.slice(0, -1)) parent.addChild(path[path.length - 1], newModule) } // 若是當前模塊存在子模塊(modules字段) // 遍歷子模塊,逐個註冊,最終造成一個樹 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } } } // module.js export default class Module { constructor (rawModule, runtime) { // 初始化時runtime爲false this.runtime = runtime // Store some children item // 用於保存子模塊 this._children = Object.create(null) // Store the origin module object which passed by programmer // 保存原來的moudle,在Store的installModule中會處理actions、mutations等 this._rawModule = rawModule const rawState = rawModule.state // Store the origin module's state // 保存state this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } addChild (key, module) { // 將子模塊添加到_children中 this._children[key] = module } }
這裏調用ModuleCollection
構造函數,經過path
的長度判斷是否爲根module,首先進行根module的註冊,而後遞歸遍歷全部的module,子module 添加其父module的_children屬性上,最終造成一棵樹
接着,仍是一些變量的初始化,而後
// 綁定commit和dispatch的this指針 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) }
這裏會將dispath和commit方法的this指針綁定爲store,好比下面這樣的騷操做,也不會影響到程序的運行
this.$store.dispatch.call(this, 'someAction', payload)
接着是store的核心代碼
// 這裏是module處理的核心,包括處理根module、命名空間、action、mutation、getters和遞歸註冊子module installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length /* * { * // ... * modules: { * moduleA: { * namespaced: true * }, * moduleB: {} * } * } * moduleA的namespace -> 'moduleA/' * moduleB的namespace -> '' */ const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { // 保存namespaced模塊 store._modulesNamespaceMap[namespace] = module } // set state if (!isRoot && !hot) { // 非根組件設置state // 根據path獲取父state const parentState = getNestedState(rootState, path.slice(0, -1)) // 當前的module const moduleName = path[path.length - 1] store._withCommit(() => { // 使用Vue.set將state設置爲響應式 Vue.set(parentState, moduleName, module.state) }) console.log('end', store) } // 設置module的上下文,從而保證mutation和action的第一個參數能拿到對應的state getter等 const local = module.context = makeLocalContext(store, namespace, path) // 逐一註冊mutation module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) // 逐一註冊action module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) // 逐一註冊getter module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) // 逐一註冊子module module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) }) }
首先保存namespaced
模塊到store._modulesNamespaceMap
,再判斷是否爲根組件且不是hot,獲得父級module的state和當前module的name,調用Vue.set(parentState, moduleName, module.state)
將當前module的state掛載到父state上。接下去會設置module的上下文,由於可能存在namespaced
,須要額外處理
// 設置module的上下文,綁定對應的dispatch、commit、getters、state function makeLocalContext (store, namespace, path) { // namespace 如'moduleA/' const noNamespace = namespace === '' const local = { // 若是沒有namespace,直接使用原來的 dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { // 統一格式 由於支持payload風格和對象風格 const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args // 若是root: true 不會加上namespace 即在命名空間模塊裏提交根的 action 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 } } // 觸發action return store.dispatch(type, payload) }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { // 統一格式 由於支持payload風格和對象風格 const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args // 若是root: true 不會加上namespace 即在命名空間模塊裏提交根的 mutation 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 } } // 觸發mutation store.commit(type, payload, options) } } // getters and state object must be gotten lazily // because they will be changed by vm update // 這裏的getters和state須要延遲處理,須要等數據更新後才進行計算,因此使用getter函數,當訪問的時候再進行一次計算 Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) // 獲取namespace下的getters }, state: { get: () => getNestedState(store.state, path) } }) return local } function makeLocalGetters (store, namespace) { const gettersProxy = {} const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // 若是getter不在該命名空間下 直接return if (type.slice(0, splitPos) !== namespace) return // 去掉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. // 給getters加一層代理 這樣在module中獲取到的getters不會帶命名空間,實際返回的是store.getters[type] type是有命名空間的 Object.defineProperty(gettersProxy, localType, { get: () => store.getters[type], enumerable: true }) }) return gettersProxy }
這裏會判斷module
的namespace
是否存在,不存在不會對dispatch
和commit
作處理,若是存在,給type
加上namespace
,若是聲明瞭{root: true}
也不作處理,另外getters
和state
須要延遲處理,須要等數據更新後才進行計算,因此使用Object.defineProperties
的getter函數,當訪問的時候再進行計算
再回到上面的流程,接下去是逐步註冊mutation
action
getter
子module
,先看註冊mutation
/* * 參數是store、mutation的key(namespace處理後的)、handler函數、當前module上下文 */ function registerMutation (store, type, handler, local) { // 首先判斷store._mutations是否存在,不然給空數組 const entry = store._mutations[type] || (store._mutations[type] = []) // 將mutation包一層函數,push到數組中 entry.push(function wrappedMutationHandler (payload) { // 包一層,commit執行時只須要傳入payload // 執行時讓this指向store,參數爲當前module上下文的state和用戶額外添加的payload handler.call(store, local.state, payload) }) }
mutation
的註冊比較簡單,主要是包一層函數,而後保存到store._mutations
裏面,在這裏也能夠知道,mutation
能夠重複註冊,不會覆蓋,當用戶調用this.$store.commit(mutationType, payload)
時會觸發,接下去看看commit
函數
// 這裏的this已經被綁定爲store commit (_type, _payload, _options) { // 統一格式,由於支持對象風格和payload風格 const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 獲取當前type對應保存下來的mutations數組 const entry = this._mutations[type] if (!entry) { // 提示不存在該mutation if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } // 包裹在_withCommit中執行mutation,mutation是修改state的惟一方法 this._withCommit(() => { entry.forEach(function commitIterator (handler) { // 執行mutation,只須要傳入payload,在上面的包裹函數中已經處理了其餘參數 handler(payload) }) }) // 執行mutation的訂閱者 this._subscribers.forEach(sub => sub(mutation, this.state)) if ( process.env.NODE_ENV !== 'production' && options && options.silent ) { // 提示silent參數已經移除 console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } }
首先對參數進行統一處理,由於是支持對象風格和載荷風格的,而後拿到當前type
對應的mutation數組,使用_withCommit
包裹逐一執行,這樣咱們執行this.$store.commit
的時候會調用對應的mutation
,並且第一個參數是state
,而後再執行mutation
的訂閱函數
接下去看action
的註冊
/* * 參數是store、type(namespace處理後的)、handler函數、module上下文 */ function registerAction (store, type, handler, local) { // 獲取_actions數組,不存在即賦值爲空數組 const entry = store._actions[type] || (store._actions[type] = []) // push到數組中 entry.push(function wrappedActionHandler (payload, cb) { // 包一層,執行時須要傳入payload和cb // 執行action 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 ) // 若是action的執行結果不是promise,將他包裹爲promise,這樣就支持promise的鏈式調用 if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { // 使用devtool處理一次error return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
和mutation
很相似,使用函數包一層而後push到store._actions
中,有些不一樣的是執行時參數比較多,這也是爲何咱們在寫action
時能夠解構拿到commit
等的緣由,而後再將返回值promisify
,這樣能夠支持鏈式調用,但實際上用的時候最好仍是本身返回promise
,由於一般action
是異步的,比較多見是發起ajax請求,進行鏈式調用也是想當異步完成後再執行,具體根據業務需求來。接下去再看看dispatch
函數的實現
// this已經綁定爲store dispatch (_type, _payload) { // 統一格式 const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } // 獲取actions數組 const entry = this._actions[type] // 提示不存在action if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } // 執行action的訂閱者 this._actionSubscribers.forEach(sub => sub(action, this.state)) // 若是action大於1,須要用Promise.all包裹 return entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) }
這裏和commit
也是很相似的,對參數統一處理,拿到action數組,若是長度大於一,用Promise.all
包裹,不過直接執行,而後返回執行結果。
接下去是getters
的註冊和子module
的註冊
/* * 參數是store、type(namesapce處理後的)、getter函數、module上下文 */ function registerGetter (store, type, rawGetter, local) { // 不容許重複定義getters if (store._wrappedGetters[type]) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate getter key: ${type}`) } return } // 包一層,保存到_wrappedGetters中 store._wrappedGetters[type] = function wrappedGetter (store) { // 執行時傳入store,執行對應的getter函數 return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } }
首先對getters
進行判斷,和mutation
是不一樣的,這裏是不容許重複定義的,而後包裹一層函數,這樣在調用時只須要給上store
參數,而用戶的函數裏會包含local.state
local.getters
store.state
store.getters
// 遞歸註冊子module installModule(store, rootState, path.concat(key), child, hot)
接着再繼續執行resetStoreVM(this, state)
,將state
和getters
存放到一個vue實例
中,
// initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state)
function resetStoreVM (store, state, hot) { // 保存舊vm const oldVm = store._vm // bind store public getters store.getters = {} const wrappedGetters = store._wrappedGetters const computed = {} // 循環全部getters,經過Object.defineProperty方法爲getters對象創建屬性,這樣就能夠經過this.$store.getters.xxx訪問 forEachValue(wrappedGetters, (fn, key) => { // use computed to leverage its lazy-caching mechanism // getter保存在computed中,執行時只須要給上store參數,這個在registerGetter時已經作處理 computed[key] = () => fn(store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) // 使用一個vue實例來保存state和getter // silent設置爲true,取消全部日誌警告等 const silent = Vue.config.silent Vue.config.silent = true store._vm = new Vue({ data: { $$state: state }, computed }) // 恢復用戶的silent設置 Vue.config.silent = silent // enable strict mode for new vm // strict模式 if (store.strict) { enableStrictMode(store) } // 若存在oldVm,解除對state的引用,等dom更新後把舊的vue實例銷燬 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()) } }
這裏會從新設置一個新的vue實例,用來保存state
和getter
,getters
保存在計算屬性中,會給getters
加一層代理,這樣能夠經過this.$store.getters.xxx
訪問到,並且在執行getters時只傳入了store
參數,這個在上面的registerGetter
已經作了處理,也是爲何咱們的getters
能夠拿到state
getters
rootState
rootGetters
的緣由。而後根據用戶設置開啓strict
模式,若是存在oldVm,解除對state的引用,等dom更新後把舊的vue實例銷燬
function enableStrictMode (store) { store._vm.$watch( function () { return this._data.$$state }, () => { if (process.env.NODE_ENV !== 'production') { // 不容許在mutation以外修改state assert( store._committing, `Do not mutate vuex store state outside mutation handlers.` ) } }, { deep: true, sync: true } ) }
使用$watch
來觀察state
的變化,若是此時的store._committing
不會true,即是在mutation
以外修改state,報錯。
再次回到構造函數,接下來是各種插件的註冊
// apply plugins plugins.forEach(plugin => plugin(this)) if (Vue.config.devtools) { devtoolPlugin(this) }
到這裏store
的初始化工做已經完成。大概長這個樣子
看到這裏,相信已經對store
的一些實現細節有所瞭解,另外store
上還存在一些api,可是用到的比較少,能夠簡單看看都有些啥
用於監聽一個getter
值的變化
watch (getter, cb, options) { if (process.env.NODE_ENV !== 'production') { assert( typeof getter === 'function', `store.watch only accepts a function.` ) } return this._watcherVM.$watch( () => getter(this.state, this.getters), cb, options ) }
首先判斷getter
必須是函數類型,使用$watch
方法來監控getter
的變化,傳入state
和getters
做爲參數,當值變化時會執行cb回調。調用此方法返回的函數可中止偵聽。
用於修改state,主要用於devtool插件的時空穿梭功能,代碼也至關簡單,直接修改_vm.$$state
replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
用於動態註冊module
registerModule (path, rawModule, options = {}) { if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) assert( path.length > 0, 'cannot register the root module by using registerModule.' ) } this._modules.register(path, rawModule) installModule( this, this.state, path, this._modules.get(path), options.preserveState ) // reset store to update getters... resetStoreVM(this, this.state) }
首先統一path
的格式爲Array,接着是斷言,path只接受String
和Array
類型,且不能註冊根module,而後調用store._modules.register
方法收集module,也就是上面的module-collection
裏面的方法。再調用installModule
進行模塊的安裝,最後調用resetStoreVM
更新_vm
根據path
註銷module
unregisterModule (path) { if (typeof path === 'string') path = [path] if (process.env.NODE_ENV !== 'production') { assert(Array.isArray(path), `module path must be a string or an Array.`) } this._modules.unregister(path) this._withCommit(() => { const parentState = getNestedState(this.state, path.slice(0, -1)) Vue.delete(parentState, path[path.length - 1]) }) resetStore(this) }
和registerModule
同樣,首先統一path
的格式爲Array,接着是斷言,path只接受String
和Array
類型,接着調用store._modules.unregister
方法註銷module
,而後在store._withCommit
中將該module
的state
經過Vue.delete
移除,最後調用resetStore
方法,須要再看看resetStore
的實現
function resetStore (store, hot) { store._actions = Object.create(null) store._mutations = Object.create(null) store._wrappedGetters = Object.create(null) store._modulesNamespaceMap = Object.create(null) const state = store.state // init all modules installModule(store, state, [], store._modules.root, true) // reset vm resetStoreVM(store, state, hot) }
這裏是將_actions
_mutations
_wrappedGetters
_modulesNamespaceMap
都清空,而後調用installModule
和resetStoreVM
從新進行所有模塊安裝和_vm
的設置
用於執行mutation
_withCommit (fn) { const committing = this._committing this._committing = true fn() this._committing = committing }
在執行mutation
的時候,會將_committing
設置爲true,執行完畢後重置,在開啓strict
模式時,會監聽state
的變化,當變化時_committing
不爲true時會給出警告
爲了不每次都須要經過this.$store
來調用api,vuex
提供了mapState
mapMutations
mapGetters
mapActions
createNamespacedHelpers
等api,接着看看各api的具體實現,存放在src/helpers.js
下面這些工具函數是輔助函數內部會用到的,能夠先看看功能和實現,主要作的工做是數據格式的統1、和經過namespace
獲取module
/** * 統一數據格式 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] * @param {Array|Object} map * @return {Object} */ function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) } /** * 返回一個函數,接受namespace和map參數,判斷是否存在namespace,統一進行namespace處理 * @param {Function} fn * @return {Function} */ 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) } } /** * 根據namespace獲取module * @param {Object} store * @param {String} helper * @param {String} namespace * @return {Object} */ function getModuleByNamespace (store, helper, namespace) { const module = store._modulesNamespaceMap[namespace] if (process.env.NODE_ENV !== 'production' && !module) { console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`) } return module }
爲組件建立計算屬性以返回 store
中的狀態
export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { // 返回一個對象,值都是函數 res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { // 若是存在namespace,拿該namespace下的module const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } // 拿到當前module的state和getters state = module.context.state getters = module.context.getters } // Object類型的val是函數,傳遞過去的參數是state和getters return typeof val === 'function' ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
mapState
是normalizeNamespace
的返回值,從上面的代碼能夠看到normalizeNamespace
是進行參數處理,若是存在namespace
便加上命名空間,對傳入的states
進行normalizeMap
處理,也就是數據格式的統一,而後遍歷,對參數裏的全部state
都包裹一層函數,最後返回一個對象
大概是這麼回事吧
export default { // ... computed: { ...mapState(['stateA']) } // ... }
等價於
export default { // ... computed: { stateA () { return this.$store.stateA } } // ... }
將store
中的 getter
映射到局部計算屬性中
export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} normalizeMap(getters).forEach(({ key, val }) => { // this namespace has been mutate by normalizeNamespace val = namespace + val res[key] = function mappedGetter () { if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { return } if (process.env.NODE_ENV !== 'production' && !(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 })
一樣的處理方式,遍歷getters
,只是這裏須要加上命名空間,這是由於在註冊時_wrapGetters
中的getters
是有加上命名空間的
建立組件方法提交 mutation
export const mapMutations = normalizeNamespace((namespace, mutations) => { const res = {} normalizeMap(mutations).forEach(({ key, val }) => { // 返回一個對象,值是函數 res[key] = function mappedMutation (...args) { // Get the commit method from store let commit = this.$store.commit if (namespace) { const module = getModuleByNamespace(this.$store, 'mapMutations', namespace) if (!module) { return } commit = module.context.commit } // 執行mutation, return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) } }) return res })
和上面都是同樣的處理方式,這裏在判斷是否存在namespace
後,commit
是不同的,上面能夠知道每一個module
都是保存了上下文的,這裏若是存在namespace
就須要使用那個另外處理的commit
等信息,另外須要注意的是,這裏不須要加上namespace
,這是由於在module.context.commit
中會進行處理,忘記的能夠往上翻,看makeLocalContext
對commit
的處理
建立組件方法分發 action
export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (...args) { // get dispatch function from store let dispatch = this.$store.dispatch if (namespace) { const module = getModuleByNamespace(this.$store, 'mapActions', namespace) if (!module) { return } dispatch = module.context.dispatch } return typeof val === 'function' ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this.$store, [val].concat(args)) } }) return res })
和mapMutations
基本同樣的處理方式
Vuex
中能夠傳入plguins
選項來安裝各類插件,這些插件都是函數,接受store
做爲參數,Vuex
中內置了devtool
和logger
兩個插件,
// 插件註冊,全部插件都是一個函數,接受store做爲參數 plugins.forEach(plugin => plugin(this)) // 若是開啓devtools,註冊devtool if (Vue.config.devtools) { devtoolPlugin(this) }
// devtools.js const devtoolHook = typeof window !== 'undefined' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return store._devtoolHook = devtoolHook // 觸發vuex:init devtoolHook.emit('vuex:init', store) // 時空穿梭功能 devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) }) // 訂閱mutation,當觸發mutation時觸發vuex:mutation方法,傳入mutation和state store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) }
到這裏基本vuex的流程源碼已經分析完畢,分享下本身看源碼的思路或者過程,在看以前先把官網的文檔再仔細過一遍,而後帶着問題來看源碼,這樣效率會比較高,利用chrome在關鍵點打開debugger,一步一步執行,看源碼的執行過程,數據狀態的變換。並且能夠暫時屏蔽一些沒有反作用的代碼,好比assert,這些函數通常都不會影響流程的理解,這樣也能夠儘可能減小源碼行數。剩下的就是耐心了,前先後後斷斷續續看了不少次,總算也把這份分享寫完了,因爲水平關係,一些地方可能理解錯誤或者不到位,歡迎指出。