vuex源碼分析

時隔一年了,又從新把vue源碼拿起來了,準備記錄下來,也算是對本身知識的一個鞏固。vue

Vuex初始化

vuex做爲vue的一個生態插件,也是支持npm發佈的,當咱們import的時候呢,執行的是vuex/dist/vuex.esm.js這裏面的代碼 vuex

那它是腫麼打包的呢,咱們看下build/config.js中

咱們看一下這個入口文件,

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

new Vue.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.瀏覽器

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

而後會再次執行this.register這個方法,遞歸調用。注意一點這個時候path就會有了新的變化,path.concat(key),這個key就是module前面定義的這個key值,也就是上圖中的a,b。 咱們再看當path長度不爲0的時候,就是創建父子關係的一個過程。 經過register這個邏輯呢就是將module創建一個樹狀結構。這就是執行了new ModuleCollection的一個返回結果。看一下結構圖。

下面咱們分析下installModule的實現markdown

installModule 的實現

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,因此在

圖中的state,就是咱們的這個local.state。因此這就是爲何咱們提交一個mutation的時候state就是咱們這個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 的實現

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就創建起來了。

咱們來看一下幾個語法糖

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

從這段代碼中咱們看到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呢就是

這個值, 咱們來看下normalizeMap作了些什麼操做

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

mapGetters其實和mapState很是的相似,也是經過normalizeNamespace函數來執行,將咱們的getters傳入到normalizeMap中將返回值進行遍歷,val值也是能夠是拼接出來的,

而後res[key]對應的每個key的值也就是一個mappedGetter函數,先去檢測namespace是否存在,存在就直接執行,返回this.$store.getters[val],這個val值爲拼接後的,因此咱們能夠找到對應的子模塊下的值。例如:
對應的就是store中的getters下的a的computedCount
這個呢就是咱們mapGetters作的一些事情,因爲和mapState有一些類似之處,就再也不細緻寫了。

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

咱們能夠看一下,mapMutations的這段代碼和mapState的很類似,在執行這個函數的時候會把mutations作一次normalizeMap,變成這個key,val的形式,也會去判斷namespace是否存在,若是沒有的話,commit就是咱們這個$store.commit,最終去直接調用commit的方法,若是有的話,就去getModuleNamespace方法去找到對應的module,而後找到module.context,局部的上下文找到對應的commit方法,再去提交。

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的這個方法呢也是同樣的,只不過換成了dispatch。找到了對應的dispatch,執行相應的dispatch。

這些呢就是咱們所謂的語法糖,對原有語法的一些加強,讓咱們用更少的代碼去實現一樣的功能。這也是讓咱們學習到了一點,從此在設計一些js庫的時候,從api設計角度中應該學習的方向。

相關文章
相關標籤/搜索