時隔一年了,又從新把vue源碼拿起來了,準備記錄下來,也算是對本身知識的一個鞏固。vue
vuex做爲vue的一個生態插件,也是支持npm發佈的,當咱們import的時候呢,執行的是vuex/dist/vuex.esm.js這裏面的代碼 vuex
export default { Store, install, version: '__VERSION__', mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } 複製代碼
能夠知道當咱們import vuex的時候,實際上就會返回這樣的一個對象,因此咱們vuex上也有store這個對象,當咱們使用Vue.use(Vue)的時候,就會執行這個install。咱們來看下這個install的源碼。在src/store.js中npm
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) } 複製代碼
這裏面的邏輯呢就是當咱們反覆調用的時候只會執行一次,而後執行applyMixin方法,在src/mixin.js中api
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 } } } 複製代碼
首先先會對咱們的vue版本進行一個判斷,1.x和2.x走的都是這一套代碼,只是執行的方法不同,2.x呢我看能看到是執行的Vue.mixin方法,混入beforeCreate這個鉤子函數,而後混入了vuexInit,在vuexInit的時候呢,其實就是把咱們的store注入進來,咱們會看到先去取這個this.$options,當options.store存在的時候呢,判斷store是不是函數,若是是函數就去執行,若是不是就直接賦值,若是沒有的話,就去找它的parent.$store,經過這種方式,能讓咱們每個vue實例都有一個$store對象。這樣咱們能夠在任意的組件中經過this.$store能夠訪問到咱們的store實例。那麼咱們這個store實際上是在new Vue的時候傳入的。數組
const app = new Vue({ el:'#app', store }) 複製代碼
對於Store的定義呢是在src/store.js中,咱們來分析一下store的這個構造函數的執行邏輯promise
let Vue // bind on install export class Store { constructor(options ={}){ if (!Vue && typeof window !== 'undefined' && window.Vue) { install(window.Vue) } /* 當咱們不經過npm的方法去開發,面是經過外鏈的方式去加載vue,vuex,會在window上註冊Vue這個變量,而後須要手動去執行install方法。 */ 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.`) } /* 這幾行呢就是若是是在非生產環境下有這麼幾個斷言,首先會斷言Vue,這個Vue呢就是最頭上咱們定義的這個Vue,這個Vue其實在執行install方法的時候就會賦值。斷言的意義呢就是執行store實例化前呢,咱們必定要經過Vue.vue(Vuex)去註冊,註冊完了才能實例化。 下面也會對Promise進行斷言,由於咱們Vuex整個庫是依賴promise,若是咱們的瀏覽器沒有原生支持promise,須要打一個promise的一個補丁。 最後一個是判斷this是咱們Store的一個實例,咱們去執行Store的構造函數的時候,必須是經過new的方式,若是直接調用store的函數就會報出警告。 */ const { plugins = [], strict = false } = options /* 定義了一些options的常量,plugins是vuex支持的一些個插件 */ // 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) //初始化modules的邏輯 this._modulesNamespaceMap = Object.create(null) this._subscribers = [] this._watcherVM = new Vue() /*這些就是在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) } /* 定義了一個store,緩存了this,而後經過這個this拿到了dispatch,commit方法,而後從新給它賦值,當咱們在執行dispatch的時候,它的上下文就是這個store。 */ } } 複製代碼
初始化過程當中呢,重點有三個部分,第一個就是new ModuleCollection去初始化這個modules.第二個就是installModule,初始化咱們這些個actions呀,wrappedGetters,mutations呀,還有就是去執行resetStoreVM。 下面咱們來重點分析下new ModuleCollection.瀏覽器
在src/module/module-collection.js中緩存
export default class ModuleCollection { constructor (rawRootModule) { // register root module (Vuex.Store options) this.register([], rawRootModule, false) } /*咱們在執行new Module的時候呢就去執行這個constructor,而後將rawRootModule做爲參數傳入,這個就是咱們外面定義的module,而後去執行register方法*/ 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) }) } } } 複製代碼
這裏面呢,咱們new Module的方式將咱們的module定義傳入,這個new Module,在src/module/module.js中,定義了一個Module的類,稍後我會講這個,也就是說這個module轉成了一個實例。 當咱們的path長度爲0的時候,就將newModule做爲根module,而後判斷咱們是否有這個rawModule.modules,若是有的話就去遍歷這個,拿到每個對應的module,上個圖瞅一眼bash
下面咱們分析下installModule的實現markdown
installModule(this, state, [], this._modules.root)
先看下要傳的值,把store的實例傳入,而後是state,而後是path爲空數組,
function installModule (store, rootState, path, module, hot) { const isRoot = !path.length const namespace = store._modules.getNamespace(path) /* 根據這個path.length來判斷isRoot是否爲true,namesapce呢就是module-collection.js中的方法 */ // 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) }) } 複製代碼
getNamespace (path) { let module = this.root return path.reduce((namespace, key) => { module = module.getChild(key) return namespace + (module.namespaced ? key + '/' : '') }, '') } 複製代碼
經過path.reduce構造這個namespace,而後namespace又是經過module.getChild去一層層找它的子module。在找的過程當中,module.namespaced爲true的狀況下,對這個值進行一個拼接。而後拿到對應的namespace去作對應的賦值。
下面給咱們定義了一個local,關鍵點在於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 } 複製代碼
這個函數最終返回的是一個local對象,在local裏面從新定義了dispatch,commit,在函數開始將namespace判斷是否爲空賦值給了noNamespace,當noNamespace爲空的時候,實際上就是咱們的store.dispatch,下面有一個重要的點就是將咱們的type拼接給了namespace從新賦值給type,拼接完後纔會調用store.dispatch。這就是爲何咱們在使用的時候,會看到它是拼接的效果。commit也是同樣,先是作一些參數的處理,而後再去拼接namespace,getters也是這樣的思路。這個就是咱們localContext的實現。返回來的local呢會在下面四個forEach...函數會去引用。
下面來分析下這四個函數。 1.首先來看下mutation的一個註冊過程,它實際上會去執行module.js中的
forEachMutation (fn) { if (this._rawModule.mutations) { forEachValue(this._rawModule.mutations, fn) } } 複製代碼
看咱們定義的module下面有沒有mutations,若是有的話就會去遍歷。遍歷完後會去執行registerMutation函數,進行一個註冊。
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對應的數組,_mutations[type]若是不存在,就是一個空數組。而後將wrappedMutationHandler push到這個數組中,而後這個wrappedMutationHandler執行的時候會去執行handler,能夠看到handler.call的時候store是這個上下文,而後是local.state,因此在
2.而後就是來看registerAction。 它其實和mutation是相似的。咱們看到action有一個配置是action.root,若是存在不用去拼接namespace,不然仍是須要去拼接。而後去註冊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 } }) } 複製代碼
咱們看到在執行handler.call的時候,對應的context有不少參數,這也就是官網提到的
3.其次就是來看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 ) } } 複製代碼
registerGetter和其餘的就有些不同之處了。getters對應的就不是一個數組了而是一個函數,wrappedGetter。返回的是一個rawGetter。
以上呢咱們就知道了如何把action,getter,mutation進行一個註冊。其實整個呢其實就是構造了一個樹倉。以後再進行數據處理的時候咱們就能清晰的知道如何去對應的處理。
resetStoreVM(this, state) 這個時候咱們會把this._modules.root.state做爲參數進行傳入。
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()) } } 複製代碼
首先給咱們store定義了一個public getters,而後根據store._wrappedGetters拿到去計算拿到這個store.getters。而後進行遍歷這個wrappedGetters,下面呢store._vm=new Vue,這個呢是利用vue作一個響應式,裏面傳入了data和computed,?state=state,後者state是傳入的一個參數,這就是咱們訪問module.state就能訪問到store.state,當咱們訪問每個getters的時候,返回一個store._vm[key],經過計算屬性返回fn(store)的計算結果。在這裏面呢創建了state和getter的依賴關係。 最後呢就是說當咱們再次去執行這個resetStoreVm,咱們會將把以前的store.vm拿到進行保留,而後將以前的進行銷燬,而後再從新創建它的store.vm。
以上是整個實例化的一個過程。store呢就是一個數據倉庫,爲了更好的管理呢,咱們將一個大的store拆成了一些modules,整個modules是一個樹形結構。每一個module又分別定義了state,getters,mutations,actions,而後經過遞歸遍歷模塊的方式完成了它們的初始化。這個時候咱們的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) { 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 }) 複製代碼
從這段代碼中咱們看到mapState 的返回值是經過normalizeNamespace函數執行的結果。 如今咱們看一下這個函數的
/** * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. * @param {Function} fn * @return {Function} */ function normalizeNamespace (fn) { return function (namespace, map) { if (typeof namespace !== 'string') { map = namespace; namespace = ''; } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/'; } return fn(namespace, map) } } 複製代碼
咱們能夠看到return中包含兩個值,一個是namespace一個是map, 判斷若是沒有傳namespace值,就把namespace賦值給map,不然兩個參數都有的狀況下,那麼,namespace的最後一位若是不是一位的話,就自動添加一個'/', 實際上呢就是
export default { name:'App', computed:{ ...mapState([ 'count' ]), ...mapState('a',{ aCount:'count' }) } } 複製代碼
這裏面的a後面加不加'/'都無所謂了。 其實這個函數主要是對namespace和map作一下處理。最後執行一個fn,這個時候咱們去看一開始貼入的mapState的代碼。 在函數中呢,執行了一個normalizeMap,把states傳入,這個states呢就是
function normalizeMap (map) { return Array.isArray(map) ? map.map(key => ({ key, val: key })) : Object.keys(map).map(key => ({ key, val: map[key] })) } 複製代碼
這個函數也就是map而言,支持兩種類型,一種是數組,一種是對象,都是返回個key,value的形式。若是你是數組的話,就直接調用數組的map方法,例如:
normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ],
若是是一個對象呢,就用對象的keys,例如:
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
normalizeMap函數處理完以後對結果進行遍歷,若是namespace沒有值的狀況下,對val值進行一個判斷,若是不是一個函數,就直接把值返回去,若是namespace有值的狀況下,根據getModuleBynamespace這個方法去拿到這個module的值,這個方法很簡單
function getModuleByNamespace (store, helper, namespace) { var module = store._modulesNamespaceMap[namespace]; if (!module) { console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace)); } return module } 複製代碼
根據namespace,經過store._modulesNamespaceMap去拿到,舉個示例:在初始化階段呢,就會構造這個Module,
找到這個module就會進行返回,這個時候state和getters的值就是會module.context.state和module.context.getters, 也就是咱們這個local,咱們能夠去看下這個local的源碼,在vuex/src/store.js中,
const local = module.context = makeLocalContext(store, namespace, path)
在makeLocalContext函數中呢,
Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }) return local 複製代碼
也定義了local的getters和state,也就是說咱們能夠訪問到local中的數據了,
也就是a模塊下的內容,這個aCount也就是對應到模塊A下的aCount。 以上呢就是咱們mapState所作的事情。
mapGetters其實和mapState很是的相似,也是經過normalizeNamespace函數來執行,將咱們的getters傳入到normalizeMap中將返回值進行遍歷,val值也是能夠是拼接出來的,
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 } return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) } }) return res }) 複製代碼
咱們能夠看一下,mapMutations的這段代碼和mapState的很類似,在執行這個函數的時候會把mutations作一次normalizeMap,變成這個key,val的形式,也會去判斷namespace是否存在,若是沒有的話,commit就是咱們這個$store.commit,最終去直接調用commit的方法,若是有的話,就去getModuleNamespace方法去找到對應的module,而後找到module.context,局部的上下文找到對應的commit方法,再去提交。
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 }) 複製代碼
mapActions的這個方法呢也是同樣的,只不過換成了dispatch。找到了對應的dispatch,執行相應的dispatch。
這些呢就是咱們所謂的語法糖,對原有語法的一些加強,讓咱們用更少的代碼去實現一樣的功能。這也是讓咱們學習到了一點,從此在設計一些js庫的時候,從api設計角度中應該學習的方向。