The Vuex version interpreted in this article is 2.3.1

Vuex code structure

The code for Vuex is not much, but it is small and complete. Let’s take a look at the implementation details.

Source code analysis

Entrance to the file

Entry file SRC /index.js:

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

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}Copy the code

This is the EXPOSED API of Vuex, the core of which is Store, followed by Install, which is a necessary method for a Vue plug-in. Both Store and install are in the store.js file. MapState, mapMutations, mapGetters, and mapActions are four auxiliary functions, which are used to map the relevant attributes in the store to the components.

The install method

All Vuejs plug-ins should have an install method. Let’s take a look at the usual poses we use with Vuex:

import Vue from 'vue'
import Vuex from 'vuex'. Vue.use(Vuex)Copy the code

Install method source:

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)
}Copy the code

The _Vue input to the method is the Vue constructor passed in when use is used. The install method is simple. It first determines if Vue already has a value and throws an error. Here, Vue is an internal variable declared at the beginning of the code.

let Vue // bind on installCopy the code

This is to ensure that the install method is executed only once. The applyMixin method is called at the end of the install method. This method is defined in 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
    }
  }
}Copy the code

When the vUE version is >=2, a global mixin is added to the vUE, either in the init or beforeCreate phase. Global mixins added to Vue affect every component. Mixins can be mixed in different ways, with the hook function of the same name being mixed into an array and therefore being called. Also, hooks for the mixed object will precede hooks for the component itself.

Take a look at what the mixin method vueInit does: This.$options is used to get the instance’s initialization options. When a store is passed in, mount the store to the instance’s $store. In this way, we can access Vuex’s various data and states through this.$store.xxx in Vue’s component.

Store constructor

The most code in Vuex is store.js, whose constructor is the main flow of 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))}Copy the code

Again, let’s look at the usual posture for using Store so we know the method’s entry:

export default new Vuex.Store({
  state,
  mutations
  actions,
  getters,
  modules: {... }, plugins,strict: false
})Copy the code

The store constructor starts with two judgments.

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

Here assert is a method in util.js.

export function assert (condition, msg) {
  if(! condition)throw new Error(`[vuex] ${msg}`)}Copy the code

Verify that Vue exists to ensure that the store has already been installed. In addition, Vuex relies on Promise, which is also judged here. The assert function is simple, but it’s a good way to learn about programming. Here we go:

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

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

The plugins, strict, and state are obtained by deconstructing and setting default values to get the values passed in. The state passed in can also be a method, and the return value of the method is the state.

Then we define some internal variables:

// 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()Copy the code

This._research indicates the commit state, which ensures that changes to state in Vuex can only be made in the mutation callback function, and can’t be made externally. This._actions is used to store all the actions defined by the user. This._mutations is used to store all of the user-defined mutatins. This._wrappedGetters is used to hold all getters defined by the user. This._modules stores all modules defined by the user. This._modulesnamespacemap stores the mapping between a module and its namespace. This._subscribers is used to store all subscribers with mutation changes. This._watchervm is an instance of a Vue object that uses the Vue instance method $watch to observe changes. These arguments will be used later, and we’ll expand them out.

Keep reading:

// 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)
}Copy the code

Bind the Store class’s dispatch and commit methods to the current Store instance, just like the comments to the code. The implementation of Dispatch and COMMIT will be examined later. Strict indicates whether to enable the strict mode. In the strict mode, all state changes are observed. It is recommended to enable the strict mode in the development environment and to disable the strict mode in the online environment; otherwise, certain performance costs will be incurred.

At the end of the constructor:

// 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))Copy the code
The initialization core of Vuex

installModule

Using a single state tree results in all the state of the application being lumped into one large object. However, as the app gets big, the Store object becomes bloated.

To solve this problem, Vuex allows us to split the store into modules. Each module has its own state, mutation, action, getters, and even nested submodules — similarly split from top to bottom.

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)Copy the code

Before entering the installModule method, it is important to look at what the method’s input argument, this._modules.root, is.

this._modules = new ModuleCollection(options)Copy the code

SRC /module/module-collection.js and SRC /module/module.js are used here

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)})}... }Copy the code

The module-collection constructor defines the root property of the instance as a module instance. Then, iterating through options’ modules to register.

Take a look at the Module constructor:

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) || {}
  }
  ...
}Copy the code

Here the rawModule is passed one layer at a time, that is, new Store options. The _children of the module instance is currently null, and then the _rawModule and state of the instance are set.

Return to the register method of the module-collection constructor and the related methods it uses:

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
}Copy the code

The input parameter path of the get method is an array, such as [‘subModule’, ‘subsubModule’]. The reduce method is used here, and the values are set layer by layer. This.get (path.slice(0, -1)) obtains the parent module of the current module. We then call the addChild method of the Module class to add the changed Module to the _children object of the parent Module.

Then, if modules are passed to rawModule, it recurses once to register.

Take a look at the resulting _modules data structure:

I’m going to go a long way to explain the input parameters to the installModule function, and then go back to the installModule method.

constisRoot = ! path.lengthconst namespace = store._modules.getNamespace(path)Copy the code

The root module is determined by the length of the path.

Take a look at the getNamespace method:

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) = > {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : ' ')},' ')}Copy the code

The reduce method is used to add the module names. The module.namespaced argument is used to define the module, for example:

export default {
  state,
  getters,
  actions,
  mutations,
  namespaced: true
}Copy the code

So if you define a store like this, the namespace of your selectLabelRule will be ‘selectLabelRule/’.

export default new Vuex.Store({
  state,
  actions,
  getters,
  mutations,
  modules: {
    selectLabelRule
  },
  strict: debug
})Copy the code

Next, look at the installModule method:

// register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }Copy the code

If namespaced is passed true, put the module on an internal variable _modulesNamespaceMap object based on its namespace.

then

// set state
if(! isRoot && ! hot) {const parentState = getNestedState(rootState, path.slice(0.- 1))
  const moduleName = path[path.length - 1]
  store._withCommit((a)= > {
    Vue.set(parentState, moduleName, module.state)
  })
}Copy the code

GetNestedState is similar to the previous getNamespace. It also uses Reduce to obtain the state of the current parent Module, and finally calls wue. set to add the state to the state of the parent module.

Look at the _withCommit method here:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}Copy the code

“This._research” is declared in the Store constructor with an initial value of false. Here, since we’re modifying state, all changes to state in Vuex are wrapped in _withCommit, ensuring that this. _research is true throughout the synchronization of the state change. This way when we look at the state change, if this._research is not true, we can check that this state change is problematic.

In Vuex source code example/shopping-cart for example, open store/index.js, and there is a code like this:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})Copy the code

We have two sub-modules, Cart and Products. We open store/modules/cart.js and look at the state definition in the Cart module.

const state = {
  added: [].checkoutStatus: null
}Copy the code

Run the project, open the browser, and use the debugging tool of Vue to see the state in Vuex, as shown below:

Look at the end of the installModule method:

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)
})Copy the code

Local is the input parameter for the next few methods, so we need to look at the makeLocalContext method again:

/** * 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 + typeif(! 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 + typeif(! 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
        ? (a)= > store.getters
        : (a)= > makeLocalGetters(store, namespace)
    },
    state: {
      get: (a)= > getNestedState(store.state, path)
    }
  })

  return local
}Copy the code

As the annotation for the method says, the method is used to get local dispatches, commits, getters, and states, and if there is no namespace, the root store dispatch, commit, and so on

Take local.dispath as an example. If no namespace is set to “, use this.dispatch. If there is a namespace, add namespace to type and dispath.

With the local parameter, register the mutation, action and getter, respectively. Take registration mutation as an example:

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

Find the array in the internal variable _mutations based on the name mutation. Then push the mutation’s return function into it. For example, there is such a mutation:

mutation: {
  increment (state, n) {
    state.count += n
  }
}Copy the code

They’re going to put their callback in _mutations[increment].

commit

Mutation is put on the _mutations object. Next, the Store constructor initially places the Dispatch and commit of the Store class on the current instance. What is the execution of committing a 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((a)= > {
      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')}}Copy the code

The method starts with unifyObjectStyle, because there are two ways to pass a parameter to commit:

store.commit('increment', {
  amount: 10
})Copy the code

Another way to commit a mutation is to use an object that contains the type attribute directly:

store.commit({
  type: 'increment'.amount: 10
})Copy the code
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 The ${typeof type}. `)

  return { type, payload, options }
}Copy the code

If an object is passed in, the argument is converted. Then determine whether the mutation that needs commit has been registered, this._mutations[type], if not, the error is thrown. Each mutation callback in _mutations is then called through a loop. The subscribe callback function for each mutation is then executed.

Vuex helper function

Vuex provides four helper functions:

Using mapGetters as an example, look at the use of mapGetters:

The code is in 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(! (valin 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 (typeofnamespace ! = ='string') {
      map = namespace
      namespace = ' '
    } else if (namespace.charAt(namespace.length - 1)! = ='/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}Copy the code

The normalizeNamespace method uses functional programming to receive a method and return a method. MapGetters take an array or an object as an argument:

computed: {
// Use the object expansion operator to mix getters into a computed object. mapGetters(['doneTodosCount'.'anotherGetter'.// ...])}Copy the code
mapGetters({
  / / this mapping. DoneCount for store. Getters. DoneTodosCount
  doneCount: 'doneTodosCount'
})Copy the code

This is the case without passing a namespace, so let’s look at the actual implementation of this method. NormalizeNamespace starts with an argument jump, passing in an array or object with a namespace of “”, then fn(namespace, map) is executed, and then normalizeMap returns an array of the form:

{
  key: doneCount,
  val: doneTodosCount
}Copy the code

We then plug the method onto the res object to get an object of the following form:

{
  doneCount: function() {
    return this.$store.getters[doneTodosCount]
  }
}Copy the code

This is what mapGetters originally wanted:

After the

by kaola/fangwentian