逐行級源碼分析系列(一) Vuex

前言

首先這篇文章也是本人第一次發這種技術文章,錯別字,分析錯誤,不知道的東西在所不免,但願你們指正,目前本人仍是一位即將大四的學生,寫這個系列的目的也是爲了記錄在源碼中學習,經過寫博客讓我更加熟悉了源碼及其內部完完整整的實現,經過這篇文章也讓我對vuex的源碼變得很是熟悉,在寫這完篇文章以前,由於時間緣由,斷斷續續寫了兩個星期,雖然已經看完了,可是要所有分析完並寫出來,太耗費精力和時間。而後這個系列我打算按照這個順序來寫,我會堅持寫下來。排版可能有點差,我會慢慢學習,若是裏面有錯誤,大佬輕噴。。javascript

  • 逐行級源碼分析系列(一) Vuex 源碼
  • 逐行級源碼分析系列(二) Redux和React-Redux源碼
  • 逐行級源碼分析系列(三) Vue-Router源碼
  • 逐行級源碼分析系列(四) React-Router-Dom源碼
  • 逐行級源碼分析系列(五) Express源碼
  • 逐行級源碼分析系列(六) Koa核心源碼
  • 逐行級源碼分析系列(七) Typescript版Axios源碼

install

當咱們使用Vue.use會調用vuex的install方法,它的實現以下html

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構造函數,而後判斷若是_Vue === Vue,則說明已經安裝過了就直接返回,不作處理。而後調用了applyMixin(Vue)方法,咱們來看下applyMixin方法實現vue

applyMixin

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    // 混入beforeCreate,vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    // 重寫_init方法,把vuexInit方法,掛載到options中
    Vue.prototype._init = function (options = {}) {
      // 這裏作了兼容處理,若是有其餘庫也使用了init方法,就把vuexInit添加到Init數組中
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /** * Vuex init hook, injected into each instances init hooks list. */

   // 這個方法的做用就是可讓每一個組件都能經過this.$store放問到store對象
  function vuexInit () {
    // 獲取mergeoptios選線
    const options = this.$options
    // 若是存在store屬性
    if (options.store) {
      // 若是store是一個方法,就調用store,不然直接使用
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 獲取父親的$store屬性
      this.$store = options.parent.$store
    }
  }
}

複製代碼

其實整個函數看起來彷佛有點複雜java

Vue.mixin({ beforeCreate: vuexInit })
複製代碼

其實只是調用了這段代碼,由於這是vue2.0版本及以上纔有的方法,咱們這裏只討論vue2.0的狀況,關於mixin的用法,這裏不作介紹,它爲全部的組件添加beforeCreate生命週期鉤子react

下面咱們看一下vuexInit方法的實現ios

// 這個方法的做用就是可讓每一個組件都能經過this.$store放問到store對象
  function vuexInit () {
    // 獲取mergeoptions的選項
    const options = this.$options
    // 這段if邏輯其實實在根組件中,添加了一個store屬性,並賦給this.$store
    if (options.store) {
      // 若是store是一個方法,就調用store,不然直接使用
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 獲取父親的$store屬性
      this.$store = options.parent.$store
    }
  }
複製代碼

首先,獲取了this.$options,這段代碼,若是你們有看過vue源碼的應該知道,這是mergeOptions後的options, 先是判斷是否存在store屬性,若是不存在,就在父組件中查找,若是有就使用父組件中的$store,經過這種方式,可以在組件之間造成一種鏈式查找,其實本質上是引用了,根組件中的store,舉個例子web

new Vue({
  router,
  store,   // $store實際最終指向的都是這裏的store
  render: h => h(App)
}).$mount('#app')
複製代碼

new Vuex.Store(options)

安裝install完成以後,咱們來看看new Vuex.Store(options)發生了什麼,因爲源碼太多,就只截取構造函數中的代碼,一塊兒來看,vuex進行了哪些初始化操做面試

constructor (options = {}) {
    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 //使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。
    } = options


    this._committing = false   // 正在提交
    this._actions = Object.create(null)  // actions對象
    this._actionSubscribers = []  // actions訂閱數組
    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()

    // 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

    // 根module的state屬性
    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)
    }
  }

複製代碼
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.`)
    }
複製代碼

這段代碼咱們不作討論,相信你們也知道什麼意思vuex

const {
     //一個數組,包含應用在 store 上的插件方法。這些插件直接接收 store 做爲惟一參數,能夠監聽 mutation(用於外部地數據持久化、記錄或調試)或者提交 mutation (用於內部數據,例如 websocket 或 某些觀察者)
      plugins = [],
        
        
      strict = false //使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤。
    } = options
複製代碼

上面這段代碼,獲取了咱們傳入的配置pluginsstrict,上面代碼中標註有每一個屬性的做用,關於詳細的使用能夠到官網查看,之後會有講解express

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()
複製代碼

這些代碼作了一些屬性的初始化,咱們暫且不看具體是幹什麼用的,關鍵是下面這段代碼

this._modules = new ModuleCollection(options) 
複製代碼

看到這段代碼,咱們確定能立馬想到,咱們傳入的modules配置,咱們來看看modules作了哪些初始化

new ModuleCollection(options)

constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }
複製代碼

這個類的構造函數只有簡簡單單的一行代碼,它的參數rawRootModule,是咱們給Vuex.Store(options)傳入的完整的options,接下來看看register方法作了什麼

register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    // 建立Module對象,初始runtime爲false
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
      // parent是根模塊
      const parent = this.get(path.slice(0, -1)) 
      // 把模塊添加到根Module對象的_children對象中,形式以下
      // _children = {
      // user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }
    // 若是options中存在modules屬性
    if (rawModule.modules) {
      // 遍歷modules都西昂
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 獲取每一個module對應的options
        /*{ modules: { user: { state, mutations }, login }, state: { }, mutations: { } }*/

        // 看到上面的形式,若是modules裏有options,繼續遞歸遍歷, 
        // path = ['user', 'login']
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
複製代碼
const newModule = new Module(rawModule, runtime)
複製代碼

代碼一上來就建立了一個Module對象,並把options做爲參數傳入,咱們繼續看看Module這個類中作了哪些操做

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    // 獲取state
    const rawState = rawModule.state

    // 若是state是個方法就調用
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

   // ...其餘方法
}

複製代碼

上面的構造函數進行了一些初始化,this.runtime記錄了是不是運行時,this._children初始化爲空對象,它主要是用來,保存當前模塊的子模塊,this._rawModule記錄了,當前模塊的配置,而後又對state進行了些處理。而後咱們大概知道了new Module作了什麼

  1. 建立了一個_children對象
  2. _rawModule記錄模塊配置

其餘並非很重要,咱們先不提,再回到new ModuleCollection(options),構造函數中

const newModule = new Module(rawModule, runtime)
複製代碼

這裏拿到了Module對象

if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
      // parent是根模塊
      const parent = this.get(path.slice(0, -1)) 
      // 把模塊添加到根Module對象的_children對象中,形式以下
      // _children = {
      // user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }
複製代碼

這是一段邏輯判斷,而這個path是在ModuleCollection構造函數中,傳入的,初始時爲空

this.register([], rawRootModule, false)  

/** * * @param {*} path 初始爲空數組 * @param {*} rawModule options * @param {*} runtime 初始爲false */
  register (path, rawModule, runtime = true) {...}
  
  
複製代碼
if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {...}
複製代碼

他把ModuleCollection對象的root屬性設置爲一個Module對象,也就是表明根module,而else中的邏輯咱們暫時不看,由於後面會有遞歸,下個週期時會進入else分支

// 若是options中存在modules屬性
    if (rawModule.modules) {
      // 遍歷modules
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 獲取每一個module對應的options

        /*{ modules: { user: { state, mutations }, login }, state: { }, mutations: { } }*/

        // 看到上面的形式,若是modules裏有options,繼續遞歸遍歷, 
        // path = ['user', 'login']
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
複製代碼

這段代碼,拿到了當前模塊的配置,注意:根模塊的配置其實就是options, 而後判斷是否存在modules,若是存在,就遍歷每一個模塊,這個forEachValue方法,其實實現很是簡單,感興趣的能夠去看一下,最終回調函數遍歷到每一個module,並獲取到module對象和它的模塊對象的key,也就是模塊名。

以後再次調用了下register方法,遞歸執行

this.register(path.concat(key), rawChildModule, runtime)
複製代碼

注意path.concat(key), path原本是空數組,在每次遞歸時都會拼接模塊的名字,這段代碼很是關鍵,後面的namespace會有用到

而後咱們再次回到register方法的開始

// 建立Module對象,初始runtime爲false
    const newModule = new Module(rawModule, runtime)
      
    if (path.length === 0) {
      // this.root = new Module(rawModule, runtime)
      this.root = newModule
    } else {
      // 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
      // parent是根模塊
      const parent = this.get(path.slice(0, -1)) 
      // 把模塊添加到根Module對象的_children對象中,形式以下
      // _children = {
      // user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }
複製代碼

依然是建立了Module對象,此時的Module已是子Module了, if-else判斷也會執行到else

if (path.length === 0) {
 		//...
    } else {
      // 若是path = ['user', 'login'], path.slice(0, -1) = ['user'] 會去掉最後一個
      // parent是根模塊
      const parent = this.get(path.slice(0, -1)) 
      // 把模塊添加到根Module對象的_children對象中,形式以下
      // _children = {
      // user: new Module(user, runtime)
      // }
      parent.addChild(path[path.length - 1], newModule)
    }
複製代碼

假如咱們有兩個module,它會獲取到除了最後一個的全部module的key列表,並調用get方法

get (path) {
    return path.reduce((module, key) => {
      // 獲取子模塊
      return module.getChild(key)
    }, this.root)
  }
複製代碼

這段是get方法的實現,它實際上是返回path對應模塊的子模塊

parent.addChild(path[path.length - 1], newModule)
複製代碼

從最後,把模塊添加到,當前模塊的_children對象中

addChild (key, module) {
    this._children[key] = module
  }
複製代碼

最後,經過ModuleCollection對象的root,就能夠拿到Module對象樹

相似這樣

new Vuex.Store({
	modules:{
	    user: {
	       modules:{
               login
           }
	    },
	    cart: {
	        
	    }
	}
})
// 模擬一下
ModuleCollection = {
    root = 根Module: {
            _children: {
                子module(user): {
                	_children: {
                        子module(login)
                    }
                 },
                子module(cart)
            }
        }
}
複製代碼

小總結:new ModuleCollection(options)在root這個屬性上掛載了一個由module對象組成的樹

咱們回到new Vuex.Store(options)時的構造函數

this._modules = new ModuleCollection(options)
複製代碼

this._modules拿到了模塊的集合

// 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)
    }
複製代碼

這段代碼,重寫了dispatchcommit方法,其實至關於調用了bind方法,我我的認爲也能夠改寫成這樣

this.dispatch = this.dispatch.bind(store, type, payload)
this.commit = this.commit.bind(store, type, payload)
複製代碼

繼續後面的步驟

this.strict = strict
複製代碼

strict使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函數之外修改 Vuex state 都會拋出錯誤

// 根module的state屬性
    const state = this._modules.root.state
複製代碼

保存根模塊的state屬性

installModule(this, state, [], this._modules.root)
複製代碼

這段代碼雖然簡短,可是很是重要,咱們來具體分析installModule方法

installModule

/** * * @param {*} store store對象 * @param {*} rootState 根module的state對象 * @param {*} path 初始爲空數組 * @param {*} module 根module對象 * @param {*} hot */
function installModule (store, rootState, path, module, hot) {
	
}
複製代碼

它的參數如上

// 若是是空數組,說明是根module
  const isRoot = !path.length
複製代碼

判斷是不是根模塊

// 返回由module名字 拼接成的字符串
  const namespace = store._modules.getNamespace(path)
複製代碼

這段代碼頗有意思,咱們來看下getNamespace方法,它在ModuleCollection類中

getNamespace (path) {
    // 根module
    let module = this.root

    return path.reduce((namespace, key) => {
      // 獲取子module
      module = module.getChild(key)
      // 若是模塊的namespace存在, 舉個列子: 一層模塊 user/, 二層模塊: user/login/
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }
複製代碼

直接作一個簡單的例子,若是咱們在每一個模塊中使用了namespaced,設置爲true,當咱們調用commit,dispatch等方法時,咱們須要這樣作

this.$store.dispatch('count/increment')

this.$store.commit('count/INCREMENT')
複製代碼

getNamespace要作的其實就是獲取到count/increment前面的count/,並返回

// 若是namespaced存在
  if (module.namespaced) {
    // 初始時store._modulesNamespaceMap[namespace]是不存在的
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // namespace對應module
    store._modulesNamespaceMap[namespace] = module
  }
複製代碼

這段代碼作的事情,就是把namespacemodule做爲key,value保存在store對象的_modulesNamespaceMap屬性上,關於這個屬性在什麼地方用,能夠參考helper.jsgetModuleByNamespace方法,這個方法是實現mapActionsmapMutations的關鍵,之後也會講到

而後是這段代碼

// 若是不是根root module ,初始時hot也不存在, 初始時hot爲ture,因此不會執行下面的
  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)
    })
  }
複製代碼

isRoot想必不用多說,就是判斷是不是根模塊,而hot這個變量又是哪裏來的呢,他是installModule方法傳入的一個參數,初始時他是空的,但這又有什麼用處呢;emmm,因爲我本身不多用到,我就很少作詳細介紹了(由於菜,因此沒用過),具體用法官方文檔有詳細介紹

咱們繼續,前面說到,hot是不存在的,而當前又是根節點,因此也不會執行這個if邏輯,可是咱們仍是要講一下,否則一會還要回來說,首先看一下getNestedState方法實現

const parentState = getNestedState(rootState, path.slice(0, -1))

// 具體實現
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

複製代碼

首先它的第一個參數是state,也就是當前模塊的state,注意不必定是rootState,不要被調用參數誤解,其實是遞歸引用的傳遞,這個函數就是判斷當前path是否爲空,若是爲空,表示它是根模塊的state,不爲空表示爲子模塊的state,要注意的是path.slice(0, -1),它獲取了除了自己模塊名以前的模塊名數組,getNestedState函數直接來講就是用來獲取父模塊的state,從字面意思也能夠理解,至於reduce的一些操做就不詳細講解了。

const moduleName = path[path.length - 1]
複製代碼

而後就是獲取了當前模塊名,接下來關鍵來了

store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
複製代碼

從字面意思,好像是跟隨commit調用?沒錯就是這樣。。

_withCommit (fn) {
 
    const committing = this._committing
    this._committing = true
    fn()
    // 從新設置以前的提交狀態
    this._committing = committing
  }
複製代碼

它就簡單的調用了傳入的回調函數,設置了先後的狀態,而後來看下回調函數的內部

parentState:父模塊的state
moduleName:當前模塊名
module.state:當前模塊的state

Vue.set(parentState, moduleName, module.state)
複製代碼

關於Vue.set方法的介紹:向響應式對象中添加一個屬性,並確保這個新屬性一樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新屬性,由於 Vue 沒法探測普通的新增屬性

也就是說,它能夠在把每一個state屬性變爲響應式,在commit以前,爲何在以前呢,由於這是初始化階段,咱們沒有主動調用commit

咱們繼續後面的代碼

// 重寫了dispatch, commit ,getter,state等方法,所有掛載到了當前模塊的context屬性上
  const local = module.context = makeLocalContext(store, namespace, path)
複製代碼

下面我將詳細講解makeLocalContext方法

makeLocalContext

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    // 若是不存在namespace,就重寫dispatch方法
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的類型
        type = namespace + type
        // 若是不使用 namespace/action的形式調用action就會報錯
        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對象,而且這些對象對dispatch,commit等方法還有state,getter進行了包裝

const noNamespace = namespace === ''
複製代碼

這段代碼用來判斷是否存在命名空間namespace,而後咱們再來看下dispatch

1.dispatch
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的類型
        type = namespace + type
        // 若是不使用 namespace/action的形式調用action就會報錯
        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)
    },
複製代碼

首先判斷是否有命名空間,若是沒有就是正常的dispatch,若是存在,則先統一對象風格unifyObjectStyle

先來看下unifyObjectStyle實現,具體講解就寫在註釋裏了

// 統一對象風格
function unifyObjectStyle (type, payload, options) {
    // 
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}
複製代碼

在看這段代碼以前,先說一下,通常來講咱們都是這樣使用dispatch

store.dispatch('incrementAsync', {
  amount: 10
})
複製代碼

但其實也能夠這樣,而且官方文檔也有例子

store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
複製代碼

知道這些咱們就繼續往下分析

if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
複製代碼

這裏是對參數進行了簡單的處理,統一處理成了咱們日常使用的模式,最後返回了相應的type, payload, options

接下來,回到makeLocalContext方法

dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args
      if (!options || !options.root) {
        // 使用namespace拼接action的類型
        type = namespace + type
        // 若是不使用 namespace/action的形式調用action就會報錯
        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)
    },
複製代碼

統一這些參數之後,又是一個if判斷,第三個參數用的也不多,可是官方文檔是有說明的,options 裏能夠有 root: true,它容許在命名空間模塊裏提交根的 mutation或action,而後返回了調用store.dispatch方法的返回值,而後咱們來看看包裝後的commit

2.commit
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)
    }
複製代碼

這段代碼和dispatch的實現很是類似,就不講解了,所作的事情就是對參數進行統一

Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })
複製代碼

而後這段代碼是把stategetter代理到了local對象上,

3.getter

判斷當前模塊是否有命名空間,若是不是,就不作任何處理,不然調用makeLocalGetters方法,並傳入store對象和namespace完整模塊字符串,至於這個namespace是什麼,能夠往前翻一翻,有具體的講解。好比user/login,表示user模塊下的login模塊的namespace。而後咱們來看看makeLocalGetters作了什麼

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}
  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // 截取getter中的namespace,若是不相等,就不作處理
    if (type.slice(0, splitPos) !== namespace) return

    // 獲取getter 的namespace後面的字符串
    const localType = type.slice(splitPos)


    Object.defineProperty(gettersProxy, localType, {
      // 把getters中的屬性方法,代理到新的對象中
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}
複製代碼

這個函數被調用說明必定是有namespace的,而後遍歷getter,此時的getter的屬性名是包含有namespace的,至於爲何會有,這個在之後的registerGetters中會有講解。而後獲取到namespace後面真實的getter屬性名,並被代理到一個新的對象中,而且被獲取時,仍然是使用了完整的namespace,舉個例子

假設模塊:  user/todo
store.getters.doSomething()   
等價於
store.getters['user/todo/doSomething']()
複製代碼

看完這些相信你們都明白了

4.state

調用了getNestedState方法,這個方法想必不用多說,前面也有講過,用來獲取模塊的父模塊state,並返回

咱們再回到一開始,調用makeLocalContext的位置, 返回的local對象,最終放在了模塊的context屬性上

const local = module.context = makeLocalContext(store, namespace, path)
複製代碼

接下來咱們繼續分析,後面的內容

registerMutation

// 遍歷mutations
  module.forEachMutation((mutation, key) => {
    // 把namespace和mutation名進行拼接
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
複製代碼

這段代碼,簡單來講就是遍歷了,當前模塊的全部mutations,並對每一個mutation調用了registerMutation方法,傳入了store對象,完整的namespace + commit名mutation函數,以及local對象,接下來看看registerMutation方法實現,至於forEachMutation方法,你們能夠本身看一下,實現也很簡單

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    // 調用mutation, 並傳入state和參數
    handler.call(store, local.state, payload)
  })
}
複製代碼

這個函數,其實是把當前模塊的mutation放在了一個_mutations對象中,那這個屬性在哪定義的呢

this._mutations = Object.create(null)
複製代碼

實際上在Store類的構造函數的時候已經初始化爲了一個空對象,registerMutation所作的事情,就是把mutationsnamespaceType,造成一個映射關係,而且mutations是一個數組,好比這樣

{
	'user/todo/INCREMENT': [
		function() {...}
	]
}
複製代碼

這裏之因此用數組的形式存儲函數,我以爲是爲了防止重複定義mutation,由於調用以後只有最後一個會生效

entry.push(function wrappedMutationHandler (payload) {
    // 調用mutation, 並傳入state和參數
    handler.call(store, local.state, payload)
  })
複製代碼

而後就是把mutation的調用放在一個函數中,傳入了state,payload,在真正調用commit的時候纔會循環調用,真實的mutation

下面咱們繼續看後面的代碼

registerAction

module.forEachAction((action, key) => {
    // namespace + type
    const type = action.root ? key : namespace + key

    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
複製代碼

這裏和前面的處理差很少,只是有個判斷,若是action存在root說明是根模塊,因此直接用key就行了,options 裏能夠有 root: true,它容許在命名空間模塊裏提交根的 mutation,不然就使用namespacekey拼接成的action名,而後咱們來看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)
    }
      // 這是給devTool用的,能夠不用關心
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

複製代碼

咱們暫且不看wrappedActionHandler函數裏面的內容,它的處理依舊和mutation的處理同樣,也是把action放在_actions對象中,而後再看wrappedActionHandler裏的內容,它調用了action,而且讓他this指向了store,傳入了,local對象中的dispatch,commit等方法還有state,getter,這不就是咱們以前看到的,通過處理後的API方法嗎。

而後它拿到action調用以後的返回值,最終返回了一個Promise.resolve(res),也就是一個Promise

經過上面這些代碼,咱們能在實際中這麼用

注意:commit, dispatch,getters,state都是當前模塊裏的方法和對象

{
	actions: {
		async increment({ commit, dispatch, getters,state, rootGetters, rootState }) {
		 	return await getData()
		}
	}
}
複製代碼

說完了registerAction,咱們來講一說registerGetter

registerGetter

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
    )
  }
}
複製代碼

一上來就是一個判斷,簡單點來講就是,不容許有重複定義的getters,咱們以前是看到actionsmutation是能夠重複定義的。而後再來看其餘的,它和以前的處理有所不一樣,但也相差不大,由於不容許有重複,因此就不須要push一個函數了,直接調用了getter方法,傳入了state,getters,根state,根getters,咱們能夠這樣用

{
	['INCREMENT']: function(state, getters, rootState, rootGetters){
		//...
	}
}
複製代碼

講完這些installModule基本上要結束了,咱們看最後一段代碼

module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
複製代碼

沒錯,是個遞歸,它拿到了子模塊進行了遞歸,你們能夠翻到前面梳理一下流程

installModule方法咱們也講完了,咱們要回到Store類的構造函數中,看看還有些什麼初始化操做

resetStoreVM(this, state)

plugins.forEach(plugin => plugin(this))

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
   devtoolPlugin(this)
}
複製代碼

接下來分析resetStoreVM

resetStoreVM

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
    // 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
    })
  })

  // 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._vm是什麼,若是有注意到這個函數中間的一段代碼的話能夠看到,_vm是又建立了一個Vue實例,這個咱們後面講。而後在store上定義了一個對象getters,而後遍歷以前,registerGetters註冊的getter,而後是這段代碼

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
    })
  })


// partial函數實現
export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}
複製代碼

首先是遍歷全部getters,調用partial函數,返回了一個新函數,並把它放入computed對象中,後面的代碼實際上是作了這件事

$store.getter
等價於
$store._vm.getter
複製代碼

getter代理到了一個新的Vue實例的computed對象上,這在後面的代碼有所體現

const silent = Vue.config.silent
  // 啓動Vue的日誌和警告
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      // 把state放在Vue的data中
      $$state: state
    },
    computed  // 把全部getter放在了computed中
  })
複製代碼

這段代碼相信不會陌生,vuex之因此可以響應式,緣由就在這裏,咱們經過調用mutation,修改了state,會觸發頁面更新,實際上是Vue的幫助

strict

咱們繼續看後面的代碼

if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
     // 強制getters從新計算
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 防止重複建立Vue實例(我的理解)
    Vue.nextTick(() => oldVm.$destroy())
  }
複製代碼

首先是判斷strict是否爲true, 表示是嚴格模式,若是直接更改state,會報錯,咱們看一下它的實現

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源碼的實現,它是如何實現修改state報錯

Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}

    options.user = true  // 很關鍵的屬性

    const watcher = new Watcher(vm, expOrFn, cb, options)

    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
複製代碼

這段代碼有個地方很關鍵,options.user = true,它被傳入了Watcher對象中,還有咱們傳入了箭頭函數cb

咱們看看Watcher哪裏有使用到user屬性

class Watcher {
    
  // ...
    
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
    
    // ...
}


複製代碼

我先說一下,這個run方法在什麼時機調用的,它是在set屬性訪問器內部調用notify以後,watcher會調用自身的update方法,而後run就會被調用,可能說的不太清楚,若是各位有時間能夠看一下,這裏只針對strict原理來說

下面咱們只看這段代碼

if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
複製代碼

咱們知道以前傳入的user屬性爲true, 若是調用回調是必定會拋出錯誤的

if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
複製代碼

這就是strict模式下,直接修改state會報錯的緣由

講完這些,其實後面的代碼就簡單略過了,也不是很重要(懶?)

而後咱們來看Store構造函數中最後一點內容

plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
   }
複製代碼

首先調用了全部的plugin,並傳入了store對象,關於plugin的用法官方文檔都有介紹。而後關於useDevtools內容我就不講解了,它和devTool相關

終於講完了初始化,咱們開始講Vuex的一些API

API

咱們按照官方文檔一個個來

1. commit

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(() => {
      // 遍歷type對應的mutation數組
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 遍歷全部訂閱,並傳入mutation對象和狀態
    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方法,統一對象風格,若是有看前面的內容的話,應該知道,這是用來處理如下兩種狀況的參數

commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)
複製代碼

而後是下面這段

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
    }
複製代碼

若是commitmutation不存在的話,就會報出警告,並返回不作處理

this._withCommit(() => {
      // 遍歷type對應的mutation數組
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
複製代碼

_withCommit方法前面也有講過,簡單點說其實就是調用傳入的回調函數,這裏循環調用了mutation,至於爲何是數組,前面有講到,是在registerMutation方法

咱們繼續來看

// 遍歷全部訂閱,並傳入mutation對象和狀態
    this._subscribers.forEach(sub => sub(mutation, this.state))
    
    // silent屬性已經被刪除,不讓使用
    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'
      )
    }
複製代碼

this._subscribers屬性也是在Store對象的構造函數初始化時建立的一個數組,看到這個數組的名字,不用多說確定是發佈訂閱模式,而後循環調用訂閱的回調函數,它是在mutation被調用後執行, 可是在哪裏訂閱的呢,實際上是在subscribe方法,它也是Vuex的一個API,下面咱們來具體講講

2. subscribe

訂閱 store 的 mutation。handler 會在每一個 mutation完成後調用,接收 mutation 和通過 mutation 後的狀態做爲參數

subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }
複製代碼
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}
複製代碼

這就是一個簡單的發佈訂閱模式的應用,把回調存儲在了訂閱數組中,其中genericSubscribe方法利用了閉包,返回了一個函數,調用它以後就能夠取消訂閱,其實還有其餘的訂閱方法,subscribeAction

3. subscribeAction

subscribeAction (fn) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers)
  }
複製代碼

判斷是不是一個函數,若是是默認爲before函數,也就是在dispatch調用action以前調用,若是是{after: fn}就會在action以後調用

4. dispatch

// 執行了beforeActions全部回調
  // 執行全部actions,並拿到全部promise返回的結果
  // 執行了afterActions全部回調
  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
    }

    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      return res
    })
  }
複製代碼

前面關於對象統一,以及是否存在action的判斷就不講了

try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }
複製代碼

而後過濾篩選獲取到了訂閱的一些before函數,也就是在調用action以前調用,並傳入了action, action = { type, payload }以及state

5. watch

響應式地偵聽 fn 的返回值,當值改變時調用回調函數。fn 接收 store 的 state 做爲第一個參數,其 getter 做爲第二個參數。最後接收一個可選的對象參數表示 Vue 的 vm.$watch 方法的參數。

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)
  }

// Store構造函數初始化時
this._watcherVM = new Vue()
複製代碼

這裏給偵聽函數裏的,getter傳入了stategetters, 當state發生變化時,偵聽函數的返回值也發生了變化,值改變後就會觸發cb回調函數, 關於vm.$watch的用法,能夠參考Vue的官方文檔vm.$watch

6. replaceState

替換 store 的根狀態,僅用狀態合併或時光旅行調試。

this._withCommit(() => {
      this._vm._data.$$state = state
    })
複製代碼

直接替換掉了$$state本來狀態

7. registerModule

能夠註冊模塊,例子:

// 註冊模塊 `myModule`
store.registerModule('myModule', {
  // ...
})
// 註冊嵌套模塊 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
複製代碼
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和一些斷言,而後調用了register方法installModule方法,resetStoreVM方法,這幾個方法前面都有講到,至關於又建立了一個Store對象,流程也差很少

8. unregisterModule

卸載一個動態模塊。

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)
  }
複製代碼

前面是對path模塊名進行了處理以及斷言是不是數組,而後調用unregister

this._modules.unregister(path)


unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
}
複製代碼

這裏獲取到了傳入模塊名,也就是path的父模塊,而後獲取子模塊判斷是否存在runtime屬性,這個屬性是幹嗎的,我也不是很清楚,但願又大佬解惑(菜 !- -,沒辦法啊)

parent.removeChild(key)

  removeChild (key) {
    delete this._children[key]
  }
複製代碼

最後刪除了子模塊,也就是咱們要刪除的模塊

9. hotUpdate

熱替換新的 action 和 mutation

官方的例子

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'

Vue.use(Vuex)

const state = { ... }

const store = new Vuex.Store({
  state,
  mutations,
  modules: {
    a: moduleA
  }
})

if (module.hot) {
  // 使 action 和 mutation 成爲可熱重載模塊
  module.hot.accept(['./mutations', './modules/a'], () => {
    // 獲取更新後的模塊
    // 由於 babel 6 的模塊編譯格式問題,這裏須要加上 `.default`
    const newMutations = require('./mutations').default
    const newModuleA = require('./modules/a').default
    // 加載新模塊
    store.hotUpdate({
      mutations: newMutations,
      modules: {
        a: newModuleA
      }
    })
  })
}
複製代碼

熱模塊更新源碼以下

hotUpdate (newOptions) {
    this._modules.update(newOptions)
    resetStore(this, true)
  }
複製代碼

this._modules.update(newOptions)方法是在module-collection.js文件中定義

update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

複製代碼
function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) {
    for (const key in newModule.modules) {
       // 若是傳入的配置中沒有該模塊就報錯
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

複製代碼

以上代碼總的來講就是遞歸遍歷模塊,並更新模塊,其中涉及到三個update方法,你們不要弄混。

update([], this.root, rawRootModule)
複製代碼

主要傳入了,一個空數組,本來的根模塊對象,要用來替換的模塊配置

function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) {
    for (const key in newModule.modules) {
       // 若是傳入的配置中沒有該模塊就報錯
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
複製代碼

遞歸遍歷,本來的模塊樹,使用新模塊替換掉本來模塊

以上代碼中還有一個模塊中的update方法,即targetModule.update(newModule)

// update target module
targetModule.update(newModule)

// module.js
update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }
複製代碼

這個方法其實很簡單,替換掉了本來的模塊。

輔助函數

mapXXX方法都在helper.js文件中

// helper.js
export const mapState = normalizeNamespace((namespace, states) => {
  //..
})

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  // ..
})
// ...
複製代碼

能夠看到他們都調用了normalizeNamespace方法,咱們知道mapXxx是一個方法,因此它必定會返回一個方法

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不是字符串,也就是說它可能不存在,namespace就設置爲一個空字符串,好比這樣

{
	computed: {
		...mapState(['username'])
	}
}
複製代碼

若是傳入了namespace字符串,而且最後沒有斜槓,就自動幫它加上,最後纔是調用真實的mapXXX,好比這樣

{
	computed: {
		...mapState('user/', ['username'])
	}
}
複製代碼

接下來咱們看一下mapState實現

mapState

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方法,傳入了咱們須要獲取的states,normalizeMap實現以下

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製代碼

這段代碼看起來可能有點複雜,舉個例子

normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap(['user', 'count']) => [ { key: 'user', val: 'user' }, { key: 'count', val: 'count' }]

normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
複製代碼

而後咱們回到以前的代碼

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返回的是一個對象,其形式以下,其餘mapMutations,mapActions均可以這樣

mapState('user', ['username', 'password'])

{
	username: function(){},
	password: function(){}
}

mapMutation('count', ['increment'])
複製代碼

如今知道爲啥mapState要寫在computed裏了吧!緣由就在這裏。爲了方便我就直接用註釋分析了

res[key] = function mappedState () {
      // store對象中的state,這個state是根state 
      let state = this.$store.state
      // 根getters
      let getters = this.$store.getters
      // 若是傳入了namespace
      if (namespace) {
        // 調用getModuleByNamespace方法,源碼實如今下方,它返回namespace對應的模塊
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 有看過前面源碼應該記得,不少方法和對象都掛載到了context屬性上
        state = module.context.state
        getters = module.context.getters
      }
      // 調用val或獲取state
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
複製代碼
function getModuleByNamespace (store, helper, namespace) {
   // _modulesNamespaceMap屬性是否是很眼熟?
   // 它是在Store類的installModule方法中使用到,記錄了namespace對應的module 
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}
複製代碼

上面這些代碼有幾個注意點

  1. getModuleByNamespace方法中的store._modulesNamespaceMap[namespace]是在installModules中進行的初始化

  2. mapState是能夠傳入回調函數的

    {
      computed: mapState({
        // 箭頭函數可以使代碼更簡練
        count: state => state.count,
    
        // 傳字符串參數 'count' 等同於 `state => state.count`
        countAlias: 'count',
    
        // 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      })
    }
    複製代碼

mapMutations

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
})
複製代碼

其餘相同的代碼就不講了,關鍵看下面的

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))
    }
複製代碼

這段代碼其實和mapState裏的相差不大,都是獲取到commit,若是有namespace就獲取模塊裏的commit,最後調用commit,它也能夠傳入一個回調函數,不過,舉個例子

methods: {
	...mapMutations(['increment']),
	//等價於
	...mapMutations({
		add: function(commit, ...args){
		   commit('increment', ...args)
	    }
	}),
     // 等價於
    ...mapMutations({
      add: 'increment' // 將 `this.add()` 映射爲 `this.$store.commit('increment')`
    })
}
// 組件中調用
this.add(1)
複製代碼

mapGetters

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
        // 若是namespace存在可是沒有找到對應的模塊 就直接返回,不作處理
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 若是沒有找到對應的getter會報錯並返回
      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
})
複製代碼

mapGetters和其它實現有所區別

  1. 全部模塊的getters都被代理在store對象中,因此直接使用getterkeynamespace拼接獲取到對應的getter;具體在哪代理能夠參見

    // store.js 的makeLocalContext方法裏的實現
    Object.defineProperties(local, {
        getters: {
          get: noNamespace
            ? () => store.getters
            : () => makeLocalGetters(store, namespace)
        },
        state: {
          get: () => getNestedState(store.state, path)
        }
      })
    複製代碼
  2. getter不支持傳入函數

mapActions

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的實現和mutation的實現如出一轍?確實是這樣。。。下面只說下用法

methods: {
	...mapActions(['increment']),
	//等價於
	...mapActions({
		add: function(dispatch, ...args){
		   dispatch('increment', ...args)
	    }
	}),
     // 等價於
    ...mapActions({
      add: 'increment' // 將 `this.add()` 映射爲 `this.$store.dispatch('increment')`
    })
}
// 組件中調用
this.add(1)
複製代碼

createNamespacedHelpers

export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})
複製代碼

官方例子

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
複製代碼

對於這個createNamespacedHelpers如何實現,我想你們應該看的懂吧

總結

終於分析完了Vuex的源碼,完成這篇文章也是沒事抽出空閒時間寫出來的,可能會有錯別字,分析錯誤或者有些我不知道的,歡迎你們指正,閱讀源碼也使我學到了不少東西,讓我從陌生,逐漸開始駕輕就熟,一直到如今,我對於源碼再也不是單純的爲了面試,而是一種興趣,謝謝你們觀看

下一章

逐行級源碼分析系列(二) Redux和React-Redux源碼(正在寫做)

未完待續。。。

相關文章
相關標籤/搜索