Vuex 源碼分析

本文解讀的Vuex版本爲2.3.1javascript

Vuex代碼結構

Vuex的代碼並很少,但麻雀雖小,五臟俱全,下面來看一下其中的實現細節。vue

源碼分析

入口文件

入口文件src/index.js:java

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}複製代碼

這是Vuex對外暴露的API,其中核心部分是Store,而後是install,它是一個vue插件所必須的方法。Store
和install都在store.js文件中。mapState、mapMutations、mapGetters、mapActions爲四個輔助函數,用來將store中的相關屬性映射到組件中。react

install方法

Vuejs的插件都應該有一個install方法。先看下咱們一般使用Vuex的姿式:vuex

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)複製代碼

install方法的源碼:編程

export function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}複製代碼

方法的入參_Vue就是use的時候傳入的Vue構造器。
install方法很簡單,先判斷下若是Vue已經有值,就拋出錯誤。這裏的Vue是在代碼最前面聲明的一個內部變量。數組

let Vue // bind on install複製代碼

這是爲了保證install方法只執行一次。
install方法的最後調用了applyMixin方法。這個方法定義在src/mixin.js中:瀏覽器

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { 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 = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}複製代碼

方法判斷了一下當前vue的版本,當vue版本>=2的時候,就在Vue上添加了一個全局mixin,要麼在init階段,要麼在beforeCreate階段。Vue上添加的全局mixin會影響到每個組件。mixin的各類混入方式不一樣,同名鉤子函數將混合爲一個數組,所以都將被調用。而且,混合對象的鉤子將在組件自身鉤子以前。數據結構

來看下這個mixin方法vueInit作了些什麼:
this.$options用來獲取實例的初始化選項,當傳入了store的時候,就把這個store掛載到實例的$store上,沒有的話,而且實例有parent的,就把parent的$store掛載到當前實例上。這樣,咱們在Vue的組件中就能夠經過this.$store.xxx訪問Vuex的各類數據和狀態了。app

Store構造函數

Vuex中代碼最多的就是store.js, 它的構造函數就是Vuex的主體流程。

constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    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

    // 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.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }複製代碼

依然,先來看看使用Store的一般姿式,便於咱們知道方法的入參:

export default new Vuex.Store({
  state,
  mutations
  actions,
  getters,
  modules: {
    ...
  },
  plugins,
  strict: false
})複製代碼

store構造函數的最開始,進行了2個判斷。

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是util.js裏的一個方法。

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}複製代碼

先判斷一下Vue是否存在,是爲了保證在這以前store已經install過了。另外,Vuex依賴Promise,這裏也進行了判斷。
assert這個函數雖然簡單,但這種編程方式值得咱們學習。
接着往下看:

const {
  plugins = [],
  strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}複製代碼

這裏使用解構並設置默認值的方式來獲取傳入的值,分別獲得了plugins, strict 和state。傳入的state也能夠是一個方法,方法的返回值做爲state。

而後是定義了一些內部變量:

// store internal state
this._committing = false
this._actions = Object.create(null)
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._committing 表示提交狀態,做用是保證對 Vuex 中 state 的修改只能在 mutation 的回調函數中,而不能在外部隨意修改state。
this._actions 用來存放用戶定義的全部的 actions。
this._mutations 用來存放用戶定義全部的 mutatins。
this._wrappedGetters 用來存放用戶定義的全部 getters。
this._modules 用來存儲用戶定義的全部modules
this._modulesNamespaceMap 存放module和其namespace的對應關係。
this._subscribers 用來存儲全部對 mutation 變化的訂閱者。
this._watcherVM 是一個 Vue 對象的實例,主要是利用 Vue 實例方法 $watch 來觀測變化的。
這些參數後面會用到,咱們再一一展開。

繼續往下看:

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

如同代碼的註釋同樣,綁定Store類的dispatch和commit方法到當前store實例上。dispatch 和 commit 的實現咱們稍後會分析。this.strict 表示是否開啓嚴格模式,在嚴格模式下會觀測全部的 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.concat(devtoolPlugin).forEach(plugin => plugin(this))複製代碼
Vuex的初始化核心

installModule

使用單一狀態樹,致使應用的全部狀態集中到一個很大的對象。可是,當應用變得很大時,store 對象會變得臃腫不堪。

爲了解決以上問題,Vuex 容許咱們將 store 分割到模塊(module)。每一個模塊擁有本身的 state、mutation、action、getters、甚至是嵌套子模塊——從上至下進行相似的分割。

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)複製代碼

在進入installModule方法以前,有必要先看下方法的入參this._modules.root是什麼。

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

這裏主要用到了src/module/module-collection.js 和 src/module/module.js

module-collection.js:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.root = new Module(rawRootModule, false)

    // register all nested modules
    if (rawRootModule.modules) {
      forEachValue(rawRootModule.modules, (rawModule, key) => {
        this.register([key], rawModule, false)
      })
    }
  }
  ...
}複製代碼

module-collection的構造函數裏先定義了實例的root屬性,爲一個Module實例。而後遍歷options裏的modules,依次註冊。

看下這個Module的構造函數:

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  ...
}複製代碼

這裏的rawModule一層一層的傳過來,也就是new Store時候的options。
module實例的_children目前爲null,而後設置了實例的_rawModule和state。

回到module-collection構造函數的register方法, 及它用到的相關方法:

register (path, rawModule, runtime = true) {
  const parent = this.get(path.slice(0, -1))
  const newModule = new Module(rawModule, runtime)
  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)
    })
  }
}

get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}

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

get方法的入參path爲一個數組,例如['subModule', 'subsubModule'], 這裏使用reduce方法,一層一層的取值, this.get(path.slice(0, -1))取到當前module的父module。而後再調用Module類的addChild方法,將改module添加到父module的_children對象上。

而後,若是rawModule上有傳入modules的話,就遞歸一次註冊。

看下獲得的_modules數據結構:

扯了一大圈,就是爲了說明installModule函數的入參,接着回到installModule方法。

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)複製代碼

經過path的length來判斷是否是root module。

來看一下getNamespace這個方法:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}複製代碼

又使用reduce方法來累加module的名字。這裏的module.namespaced是定義module的時候的參數,例如:

export default {
  state,
  getters,
  actions,
  mutations,
  namespaced: true
}複製代碼

因此像下面這樣定義的store,獲得的selectLabelRule的namespace就是'selectLabelRule/'

export default new Vuex.Store({
  state,
  actions,
  getters,
  mutations,
  modules: {
    selectLabelRule
  },
  strict: debug
})複製代碼

接着看installModule方法:

// register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }複製代碼

傳入了namespaced爲true的話,將module根據其namespace放到內部變量_modulesNamespaceMap對象上。

而後

// 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跟前面的getNamespace相似,也是用reduce來得到當前父module的state,最後調用Vue.set將state添加到父module的state上。

看下這裏的_withCommit方法:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}複製代碼

this._committing在Store的構造函數裏聲明過,初始值爲false。這裏因爲咱們是在修改 state,Vuex 中全部對 state 的修改都會用 _withCommit函數包裝,保證在同步修改 state 的過程當中 this._committing 的值始終爲true。這樣當咱們觀測 state 的變化時,若是 this._committing 的值不爲 true,則能檢查到這個狀態修改是有問題的。

看到這裏,可能會有點困惑,舉個例子來直觀感覺一下,以 Vuex 源碼中的 example/shopping-cart 爲例,打開 store/index.js,有這麼一段代碼:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})複製代碼

這裏有兩個子 module,cart 和 products,咱們打開 store/modules/cart.js,看一下 cart 模塊中的 state 定義,代碼以下:

const state = {
  added: [],
  checkoutStatus: null
}複製代碼

運行這個項目,打開瀏覽器,利用 Vue 的調試工具來看一下 Vuex 中的狀態,以下圖所示:

來看installModule方法的最後:

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 namespacedType = namespace + key
  registerAction(store, namespacedType, action, 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)
})複製代碼

local爲接下來幾個方法的入參,咱們又要跑偏去看一下makeLocalContext這個方法了:

/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */
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 (!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 (!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
}複製代碼

就像方法的註釋所說的,方法用來獲得局部的dispatch,commit,getters 和 state, 若是沒有namespace的話,就用根store的dispatch, commit等等

以local.dispath爲例:
沒有namespace爲''的時候,直接使用this.dispatch。有namespace的時候,就在type前加上namespace再dispath。

local參數說完了,接來是分別註冊mutation,action和getter。以註冊mutation爲例說明:

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})複製代碼
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(local.state, payload)
  })
}複製代碼

根據mutation的名字找到內部變量_mutations裏的數組。而後,將mutation的回到函數push到裏面。
例若有這樣一個mutation:

mutation: {
  increment (state, n) {
    state.count += n
  }
}複製代碼

就會在_mutations[increment]裏放入其回調函數。

commit

前面說到mutation被放到了_mutations對象裏。接下來看一下,Store構造函數裏最開始的將Store類的dispatch和commit放到當前實例上,那commit一個mutation的執行狀況是什麼呢?

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) {
      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 (options && options.silent) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }複製代碼

方法的最開始用unifyObjectStyle來獲取參數,這是由於commit的傳參方式有兩種:

store.commit('increment', {
  amount: 10
})複製代碼

提交 mutation 的另外一種方式是直接使用包含 type 屬性的對象:

store.commit({
  type: 'increment',
  amount: 10
})複製代碼
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)

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

若是傳入的是對象,就作參數轉換。
而後判斷須要commit的mutation是否註冊過了,this._mutations[type],沒有就拋錯。
而後循環調用_mutations裏的每個mutation回調函數。
而後執行每個mutation的subscribe回調函數。

Vuex輔助函數

Vuex提供的輔助函數有4個:

以mapGetters爲例,看下mapGetters的用法:

代碼在src/helpers.js裏:

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (!(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
})


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

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

normalizeNamespace方法使用函數式編程的方式,接收一個方法,返回一個方法。
mapGetters接收的參數是一個數組或者一個對象:

computed: {
// 使用對象展開運算符將 getters 混入 computed 對象中
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
    // ...
  ])
}複製代碼
mapGetters({
  // 映射 this.doneCount 爲 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})複製代碼

這裏是沒有傳namespace的狀況,看下方法的具體實現。
normalizeNamespace開始進行了參數跳轉,傳入的數組或對象給map,namespace爲'' , 而後執行fn(namespace, map)
接着是normalizeMap方法,返回一個數組,這種形式:

{
  key: doneCount,
  val: doneTodosCount
}複製代碼

而後往res對象上塞方法,獲得以下形式的對象:

{
  doneCount: function() {
    return this.$store.getters[doneTodosCount]
  }
}複製代碼

也就是最開始mapGetters想要的效果:

by kaola/fangwentian

相關文章
相關標籤/搜索