在通常狀況之下, 咱們廣泛使用 global event bus 來解決全局狀態共享, 組件通信的問題, 當遇到大型應用的時候, 這種方式將使代碼變得難以維護, Vuex應運而生, 接下來我將從源碼的角度分析Vuex的整個實現過程.vue
整個Vuex的目錄結構仍是很是清晰地, index.js 是整個項目的入口, helpers.js 提供Vuex的輔助方法>, mixin.js 是$store注入到vue實例的方法, util.js 是一些工具函數, store.js是store類的實現 等等, 接下來就從項目入口一步步分析整個源碼.react
首先咱們能夠從index.js看起:es6
export default { Store, install, version: '__VERSION__', mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers }
能夠看到, index.js就是導出了一個Vuex對象, 這裏能夠看到Vuex暴露的api, Store就是一個Vuex提供的狀態存儲類, 一般就是使用 new Vuex.Store(...)的方式, 來建立一個Vuex的實例. 接下來看, install 方法, 在store.js中;vuex
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 方法有個重複install的檢測報錯, 並將傳入的_Vue賦值給本身定義的Vue變量, 而這個Vue變量已經變導出, 整個項目就可使用Vue, 而不用安裝Vue;api
export let Vue
接着調用applyMixin方法, 該方法在mixin.js當中;數組
export default function (Vue) { const version = Number(Vue.version.split('.')[0]) Vue.mixin({ beforeCreate: vuexInit }) }
因此, applyMixin方法的邏輯就是全局混入一個beforeCreate鉤子函數-vuexInit;promise
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 } }
整個代碼很簡單, 就是將用戶傳入的store注入到每一個vue實例的$store屬性中去, 從而在每一個實例咱們均可以經過調用this.$store.xx訪問到Vuex的數據和狀態;app
在咱們使用Vuex的時候, 一般會實例化一個Vuex.Store類, 傳入一個對象, 對象包括state、getters、mutations、actions、modules, 而咱們實例化的時候, Vuex到底作了什麼呢? 帶着這個疑問, 咱們一塊兒來看store.js中的代碼, 首先是構造函數;ide
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)) const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools if (useDevtools) { devtoolPlugin(this) } }
構造函數一開始是判斷當window.Vue存在的時候, 調用install方法, 確保script加載的Vuex能夠正確被安裝, 接着是三個斷言函數, 確保Vue存在, 環境支持Promise, 當前環境的this是Store;函數
const { plugins = [], strict = false } = options
利用es6的賦值結構拿到options中的plugins(默認是[]), strict(默認是false), plugins 表示應用的插件、strict 表示是否開啓嚴格模式, 接着往下看;
// 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()
這裏主要是初始化一些Vuex內部的屬性, _開頭, 通常表明着私有屬性,this._committing
標誌着一個提交狀態;this._actions
存儲用戶的全部的actions;this.mutations
存儲用戶全部的mutations;this.wrappedGetters
存儲用戶全部的getters;this._subscribers
用來存儲全部對 mutation 變化的訂閱者;this._modules
表示全部modules的集合;this._modulesNamespaceMap
表示子模塊名稱記錄.
繼續往下看:
// 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
這段代碼就是經過賦值結構拿到store對象的dispatch, commit 方法, 並從新定義store的dispatch, commit 方法, 使他們的this指向store的實例, 具體的dispatch和comiit實現稍後分析.
installModule方法主要是根據用戶傳入的options, 進行各個模塊的安裝和註冊, 具體實現以下:
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`) } 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) }) }
installModules方法須要傳入5個參數, store, rootState, path, module, hot; store指的是當前Store實例, rootState是根實例的state, path當前子模塊的路徑數組, module指的是當前的安裝模塊, hot 當動態改變 modules 或者熱更新的時候爲 true。
先看這段代碼:
if (module.namespaced) { if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') { console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`) } store._modulesNamespaceMap[namespace] = module }
這段代碼主要是爲了防止子模塊命名重複, 故定義了一個map記錄每一個子模塊;
接下來看下面的代碼:
// 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)
首先, 定義一個local變量來接收makeLocalContext函數返回的結果, makeLocalContext有三個參數, store指的是根實例, namespace 指的是命名空間字符, path是路徑數組;
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 函數主要的功能就是根據是否有namespce定義不一樣的dispatch和commit, 並監聽local的getters和sate的get屬性, 那namespace是從何而來呢, 在installModule的開始:
const isRoot = !path.length const namespace = store._modules.getNamespace(path)
namespace 是根據path數組經過_modules中的getNamespace得到, 而store._modules是ModuleCollection的實例, 因此能夠到ModuleCollection中找到getNamespace方法:
getNamespace (path) { let module = this.root return path.reduce((namespace, key) => { module = module.getChild(key) return namespace + (module.namespaced ? key + '/' : '') }, '') }
該函數經過對path路徑數組reduce遍歷, 得到模塊的命名空間(eg: 'city/');,接下來是各個模塊的註冊流程, 首先看mutaiton的註冊;
module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) })
forEachMutation函數一個循環遍歷, 拿到用戶傳入的mutation函數和key值, 接着調用registerMutation函數;
// $store.state.commit('add', 1) 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) }) }
這段代碼的做用就是, 將全部的mutation函數封裝成wrappedMutationHandler存入store._mutations
這個對象當中, 咱們結合前面提過的commit的過程, 能夠更好的理解;
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) { if (process.env.NODE_ENV !== 'production') { 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 ( process.env.NODE_ENV !== 'production' && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } }
unifyObjectStyle 函數就是對參數的規範, 然後, 經過`
this._mutations[type] 拿到type所對應的全部wrappedMutationHandler函數, 遍歷執行, 傳入payload,
this._withCommit`函數在源碼中出現過不少次, 代碼以下:
_withCommit (fn) { const committing = this._committing this._committing = true fn() this._committing = committing }
代碼做用就是每次提交的時候, 將this._committing
置爲true, 執行完提交操做以後, 在從新置爲初始狀態, 確保只有mutation才能更改state的值, _subscribers相關代碼暫時不看, 咱們接下來看一看action的註冊流程:
module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) })
這段代碼和mutation的註冊流程是相似的, 不一樣在於registerAction函數
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 } }) }
能夠看到, 基於用戶的action函數, 源碼封多了一層wrappedActionHandler函數, 在action函數中, 能夠得到一個context對象, 就是在這裏作的處理, 而後, 它把action函數的執行結果封裝成了Promise並返回, 結合dispatch函數能夠更好的理解;
dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } const entry = this._actions[type] if (!entry) { if (process.env.NODE_ENV !== 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } const result = entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) return result.then(res => { return res }) }
dispatch 拿到actions後, 根據數組長度, 執行Promise.all或者直接執行, 而後經過then函數拿到promise resolve的結果.
接下來是getters的註冊
module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) })
registerGetter函數:
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 ) } }
將用戶傳入的rawGetter封裝成wrappedGetter, 放入store._wrappedGetters的對象中, 函數的執行稍後再說, 咱們繼續子模塊的安裝;
module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })
這段代碼首先是對state.modules遍歷, 遞歸調用installModule, 這時候的path是不爲空數組的, 因此會走到這個邏輯;
// 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找到它的父state, 它的模塊key就是path的最後一項, store._withCommit
上面已經解釋過了, 而後經過Vue.set 將子模塊響應式的添加到父state, 從而將子模塊都註冊完畢.
resetStoreVM 函數第一部分
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 // direct inline function use will lead to closure preserving oldVm. // using partial to return function with only arguments preserved in closure enviroment. computed[key] = partial(fn, store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) })
首先, 拿到全部的wrappedGetter函數對象, 即包裝過的用戶傳入的getters, 定義一個變量computed, 接受全部的函數, 並經過Ojbect.defineProperty
在store.getters屬性定義了get方法, 也就是說, 咱們經過this.$store.getters.xx 會訪問到 store._vm[xx], 而store._vm又是什麼呢?
// 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 // 關閉vue警告, 提醒 store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent
顯然, store._vm是一個Vue的實例, 包含全部用戶getters的計算屬性和 用戶state的$$state屬性, 而咱們訪問this.$store.state 其實就是訪問這裏的$$state屬性, 緣由在於, Store類直接定義了一個state的取值函數, 其中返回的正是這個$$state屬性;
get state () { return this._vm._data.$$state }
咱們接着看;
// enable strict mode for new vm if (store.strict) { enableStrictMode(store) }
當在Vuex嚴格模式下, strict爲true, 因此會執行enableStrictMode函數;
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 }) }
該函數利用Vue.$watch函數, 監聽$$state的變化, 當store._committing 爲false的話, 就會拋出不容許在mutation函數以外操做state;
接着咱們再來看最後一部分;
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()) }
oldVm保存着上一個store._vm對象的引用, 每次執行這個函數, 都會建立一個新的store._vm, 因此須要在這段代碼中銷燬;
至此, Store類初始化大體都講完了, 接下來分析Vuex提供的輔助函數.
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) { const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters) : state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
首先, 先說一說normalizeMap方法, 該方法主要是用於格式化參數, 用戶使用mapState函數, 可使傳入一個字符串數組, 也能夠是傳入一個對象, 通過normalizeMap方法處理, 統一返回一個對象數組;;
// normalizeMap([1,2]) => [{key: 1, val: 1}, {key: 2, val: 2}] // normalizeMap({a: 1, b: 2}) => [{key: 'a', val: 1}, {key: 'b', val: 2}] function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) }
接着, 對於處理過的對象數組遍歷, 定義了一個res對象接收, key爲鍵, mappedState方法爲值;
function mappedState () { let state = this.$store.state let getters = this.$store.getters if (namespace) { const module = getModuleByNamespace(this.$store, 'mapState', namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters) : state[val] }
整個函數代碼比較簡單, 惟一須要注意的點是, 當傳入了namespace時, 須要經過getModuleByNamespace函數找到該屬性對應的module, 還記得在installModule中, 有在store._modulesNamespaceMap
中記錄namespace和模塊間的對應關係, 所以, getModuleByNamespace就是經過這個map找到了module, 從而拿到了當前module的state和getters;
最後mapstate函數返回一個res函數對象, 用戶能夠直接利用...操做符導入到計算屬性中.
mapMutations函數和mapstate函數是相似的, 惟一的區別在於mappedMutation是commit 函數代理, 而且它須要被導入到methods;
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 } return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) }
mapActions, mapGetters 的實現也都大同小異, 便再也不具體分析.
咱們能夠經過相似這種方式使用plugins:
const myPlugin = store => { // 當 store 初始化後調用 store.subscribe((mutation, state) => { // 每次 mutation 以後調用 // mutation 的格式爲 { type, payload } } )} const store = new Vuex.Store({ // ... plugins: [myPlugin] })
在源碼當中, 能夠看到這麼一段代碼:
// apply plugins plugins.forEach(plugin => plugin(this))
即遍歷全部plugins, 傳入當前Store實例, 執行plugin函數, 所以, 示例的store參數就是Store實例, 而後示例調用store.subscribe
方法, 這是Store類暴露的一個成員方法;
subscribe (fn) { return genericSubscribe(fn, this._subscribers) }
其實這就是一個訂閱函數, 當有commit操做的時候, 就會通知全部訂閱者, 該函數返回一個函數fn, 調用這個fn便可以取消訂閱, 發佈通知代碼在commit函數中:
this._subscribers.forEach(sub => sub(mutation, this.state))
當學無所學之時, 看優秀源碼或許是一種突破瓶頸的方法, 能夠更加深刻的瞭解這個庫, 知其然, 亦知其因此然, 同時做者的一些庫的設計思想, 也會對咱們大有裨益.