Release notes

4ark.me/ Post/Vuex-s…

This article is for vuex V3.6.2 version of a source code parsing, mainly want to study the following points:

  1. Initialization of VUEX
  2. How to store the data status of VUEX
  3. What is done when a mutation is called
  4. MapState, mapActions, these binding functions

1. Initialization process

To create a vuex Store, create a new vuex.Store. To create a vuex Store, create a new vuex.

let Vue // bind on install

export class Store {
  constructor(options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if(! Vue &&typeof window! = ='undefined' && window.Vue) {
      install(window.Vue)
    }

    if (__DEV__) {
      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 } = options

    // 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)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

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

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

    constuseDevtools = options.devtools ! = =undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)}}}Copy the code

The code is not very long, so let’s take a look at what we do in this constructor.

1. Automatic installation

let Vue // bind on install

export class Store {
  constructor(options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if(! Vue &&typeof window! = ='undefined' && window.Vue) {
      install(window.Vue)
    }
  }
}

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

If the new Store has not been installed and there is already a global import Vue, it will be installed automatically, but if it has been installed, it does not need to be installed again.

When installed, applyMixin is executed. Its source code is in SRC /mixin.js:

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

Only one thing has been done: the store has been mounted to the $store, so we can access the Store from the $store in the Vue component.

2. Abnormal detection

In the case of a development environment, some checks are made:

if (__DEV__) {
  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.`)}Copy the code

3. Initialize internal variables

It then defines a series of internal variables, which will be discussed later:

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

// 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)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)

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

const state = this._modules.root.state
Copy the code

Here are a few of the more noteworthy, let’s go over each of them.

3.1. Construction modules

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

It will get the modules by new a ModuleCollection and passing in options. The ModuleCollection will recursively register all the submodules internally.

The result is a data structure that looks like this:

{
  runtime: false.state: {},
  _children: {subModule1: Module {runtime: false._children: {... },_rawModule: {... },state: {... }},subModule2: Module {runtime: false._children: {... },_rawModule: {... },state: {... }}},_rawModule: {
    modules: {
      subModule1: {state: {... },mutations: {... }}subModule2: {state: {... },mutations: {…}}
    },
  },
  namespaced: false
}
Copy the code

But don’t get too tangled up here, just get the idea.

3.2. Package Dispatches and Commits

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

The reason for wrapping the Dispatch and commit methods is to ensure that no matter how they are called, this always points to the Store instance.

Because there are so many actions in JS that can change what this refers to, either intentionally or unintentionally, vuex takes this into account.

4. Initialize the Module

We then initialize the module using the previous _module, passing in _module.root:

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

Again, it just passes in the root module, and the method detects if there are any submodules and recursively calls to initialize all of them.

Here is the implementation of installModule:

function installModule(store, rootState, path, module, hot) {
  constisRoot = ! path.lengthconst namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(
        `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
          '/'
        )}`
      )
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if(! isRoot && ! hot) {const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() = > {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
              '. '
            )}"`
          )
        }
      }
      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)
  })
}
Copy the code

4.1 Initializing the Root Module

Since this method is called recursively, let’s first look at the logic it executes when initializing the root module. First, it calls makeLocalContext to construct a context that belongs to the current module, which is the CTX argument we usually get in our action:

const local = (module.context = makeLocalContext(store, namespace, path))
Copy the code

Mutations, Actions, getters for the current module, and recursive calls to installModule if there are submodules do the same for the submodules:

module.forEachMutation(function(mutation, key) {
  var namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

module.forEachAction(function(action, key) {
  var type = action.root ? key : namespace + key
  var handler = action.handler || action
  registerAction(store, type, handler, local)
})

module.forEachGetter(function(getter, key) {
  var namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

module.forEachChild(function(child, key) {
  installModule(store, rootState, path.concat(key), child, hot)
})
Copy the code

4.2. Initialize the submodule

For submodules, some additional logic is performed:

// set state
if(! isRoot && ! hot) {var parentState = getNestedState(rootState, path.slice(0, -1))
  var moduleName = path[path.length - 1]
  store._withCommit(function() {
    if(process.env.NODE_ENV ! = ='production') {
      if (moduleName in parentState) {
        console.warn(
          '[vuex] state field "' +
            moduleName +
            '" was overridden by a module with the same name at "' +
            path.join('. ') +
            '"'
        )
      }
    }
    Vue.set(parentState, moduleName, module.state)
  })
}
Copy the code

The submodule’s state is set to the parent module’s state. This is why we can obtain the submodule’s state in this way:

console.log(this.$store.state)

/ / output
{
  subModule1: {
    count1: 0
  },
  subModule2: {
    count2: 0}}Copy the code

4.3. Initializing the namespace module

In the case of using namespaces, additional operations are performed on top of this:

var namespace = store._modules.getNamespace(path);

// register in namespace map
if (module.namespaced) {
  if(store._modulesNamespaceMap[namespace] && (process.env.NODE_ENV ! = ='production')) {
    console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/')));
  }
  store._modulesNamespaceMap[namespace] = module;
}
Copy the code

First, you get the name of the namespace by using getNamespace, which is essentially a module name followed by a /, such as subModule/. If namespaced is not enabled, you get an empty string. And then we’ll store that as a key in the _modulesNamespaceMap, and we’ll see what that does later.

4.4. Initialize the mutation, action, and getter

The initialization of mutation, action, and getter is the same for all modules.

module.forEachMutation((mutation, key) = > {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
Copy the code

First, the key of each mutation is spliced with the namespace name of the current module. Then registerMutation is called to pass in the entire store, the spliced mutation key, the mutation method, and the context of the current module. Here is the registerMutation implementation:

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

Mutations are essentially passing all of these mutations into this array store.__mutations, but here you might wonder why _mutations[type] is an array, This is because there may be multiple mutations with the same name in different modules (with no namespaces enabled), and all mutations with the same name need to be called, as is the case with action.

The initialization here is simply wrapping them around the key of the namespace and passing in some module context parameters automatically when called. Similarly, actions and getters are both wrapped up and stored in _actions and _wrappedGetters, although since actions are asynchronous, there is a little extra processing for promises.

5. Initialize state

Once the module is initialized, it processes the data in state, making it reactive, and the previously wrapped getter, making it something like computed:

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
Copy the code

Here is the implementation of resetStoreVM:

function resetStoreVM(store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  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 environment.
    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())
  }
}
Copy the code

5.1. Deal with the state

Vuex is simply a new instance of Vue to implement the state response, but there is nothing wrong with this, after all, vuex is originally dedicated to Vue:

// Cancel all logs and warnings for Vue
// 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
Copy the code

5.2. Handle getters

And the essence of a getter is a computed:

// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
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 environment.
  computed[key] = partial(fn, store)
  Object.defineProperty(store.getters, key, {
    get: () = > store._vm[key],
    enumerable: true // for local getters})})Copy the code

6. Invoke all plugins

There’s nothing to say about this, just call all the plugins, passing this:

// apply plugins
plugins.forEach((plugin) = > plugin(this))
Copy the code

7. devtools

Finally, some vue DevTools related code:

constuseDevtools = options.devtools ! = =undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)}// src/plugins/devtool.js
const target =
  typeof window! = ='undefined'
    ? window
    : typeof global! = ='undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin(store) {
  if(! devtoolHook)return

  store._devtoolHook = devtoolHook

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state'.(targetState) = > {
    store.replaceState(targetState)
  })

  store.subscribe(
    (mutation, state) = > {
      devtoolHook.emit('vuex:mutation', mutation, state)
    },
    { prepend: true }
  )

  store.subscribeAction(
    (action, state) = > {
      devtoolHook.emit('vuex:action', action, state)
    },
    { prepend: true})}Copy the code

This is the whole process of initializing VUex, but some points are just sketchy, so let’s explore the details in more detail.

Call the mutation process

Let’s talk about what happens when a mutation is called, for example, using the following code:

this.$store.commit('subModule1/increment')
Copy the code

The first step is to go to the wrapped COMMIT described earlier, which ensures that no matter how you call this, it always points to the current store instance:

this.commit = function boundCommit(type, payload, options) {
  return commit.call(store, type, payload, options)
}
Copy the code

The actual commit method is then called:

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 (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)}return
  }
  this._withCommit(() = > {
    entry.forEach(function commitIterator(handler) {
      handler(payload)
    })
  })
  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach((sub) = > sub(mutation, this.state))
  if (__DEV__ && 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

As mentioned earlier, there could be multiple mutations with the same name, so they are called in turn, but why go through the _withCommit method first? Look at its implementation:

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

If strict mode is turned on, it listens for changes to the state value and reports an error as long as the state value is not modified internally via mutation:

function enableStrictMode(store) {
  store._vm.$watch(
    function() {
      return this._data.$$state
    },
    () = > {
      if (__DEV__) {
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`)}}, {deep: true.sync: true})}Copy the code

Three, mapState, mapActions these binding function implementation

Let’s take a look at the definitions of these four helpers.js methods, which are available in the SRC /helpers.js source code:

export const mapState = normalizeNamespace((namespace, states) = > {}
export const mapMutations = normalizeNamespace((namespace, mutations) = > {}
export const mapGetters = normalizeNamespace((namespace, getters) = > {}
export const mapActions = normalizeNamespace((namespace, actions) = > {}
Copy the code

They are all handled by a method called normalizeNamespace, which, as the name implies, resolves namespaces. We know that mapXXX these methods can take one or two arguments. Normally the first argument is the namespace of the state Module. The second argument is what you want to get, but it is also possible to pass only the first argument, in which case the namespace is root.

So the implementation of this method is simple. Just determine whether the first argument is a string, if so treat it as a map, otherwise treat it as a map, and add a/to the end, which was mentioned earlier when initializing the namespace module.

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

In fact, the implementations of these four methods are similar, so here we only record the implementation of mapState:

export const mapState = normalizeNamespace((namespace, states) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(states)) {console.error(
      '[vuex] mapState: mapper parameter must be either an Array or an Object'
    )
  }
  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
})
Copy the code

The core principle is to serialize the passed states, and then get the values in the corresponding module of the current namespace, and determine whether it is a function. If it is, call the function and pass the state and getters in the current module, store the return of the function in the object, and then return.

Refer to the link

  • Vuex source code parsing