Vuex V3.0.1 is used for analysis

install

Vuex provides an install method to register vuue. Use. The install method determines the vue version.

// vuex/src/mixin.js
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)
  }
}
Copy the code

For the 1.x version, vuexInit is added to vueInit. When vUE is initialized, vuex is also initialized. For the 2.x version, a beforeCreated hook function is added globally by mixin

The vuexInit method looks like this:

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

$store = this.$store = this.$store = this.$store = this. So you can get options.store. This refers to the current Vue instance. The options.store is an instance of the store object, so you can access the store instance in the component through this

const app = new Vue({
  el: '#app'.// Provide the store object to the "store" option, which injects store instances into all child components
  store,
  components: { Counter },
  template: '<div class="app"></div>'
})
Copy the code

Store

In the beforeCreate life cycle, it will get options.store, which will also be initialized

The core of every Vuex application is the Store, so there is a store initialization process. Here is a simple store example (from Vuex’s official website) :

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
Copy the code

The Store source code is located in vuex/ SRC /store.js. Within this class’s constructor, a new vue instance is created, so vuex can use many of vue’s features, such as responsive logic for data

When you initialize a Store, you initialize modules, Dispatch, commit, etc

Initialize the Module and build the Module tree

From the Store constructor constructor, the following is the initialization entry for modules:

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

ModuleCollection is an ES6 class

// src/module/module-collection.js
constructor (rawRootModule) {
  // register root module (Vuex.Store options)
  this.register([], rawRootModule, false)}Copy the code

The constructor of this class calls the register method and the second argument, rawRootModule, is the options object passed in when the Store is initialized

// src/module/module-collection.js
register (path, rawModule, runtime = true) {
    / /... Omit irrelevant code

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

The register method contains a new Module, which is a class used to describe a single Module. It defines data structs, attributes, and methods associated with a single Module, as follows:

These methods, attributes, and so on are related to the subsequent construction of the Module Tree

Since each module has its own state, Namespaced, actions, etc., these attributes and methods are also attached to each module object during module initialization. For example, here is the code for attaching state:

// src/module/module.js
this._rawModule = rawModule
const rawState = rawModule.state

// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
Copy the code

If (rawModule. Modules) is passed as options. If (rawModule. Modules) is passed as options. So rawModule.modules is modules similar to those in ExampleA

/** * example code ExampleA */
const store = new Vuex.Store({
  / /... Omit irrelevant code
  modules: {
    profile: {
      state: { age: 18 },
      getters: {
        age (state) {
          return state.age
        }
      }
    },
    account: {
      namespaced: true.state: {
        isAdmin: true.isLogin: true
      },
      getters: {
        isAdmin (state) {
          return state.isAdmin
        }
      },
      actions: {
        login ({ state, commit, rootState }) {
          commit('goLogin')}},mutations: { goLogin (state) { state.isLogin = ! state.isLogin } },// Nested modules
      modules: {
        // Further nested namespaces
        myCount: {
          namespaced: true.state: { count: 1 },
          getters: {
            count (state) {
              return state.count
            },
            countAddOne (state, getters, c, d) {
              console.log(123, state, getters, c, d);
              return store.getters.count
            }
          },
          actions: {
            addCount ({ commit }) {
              commit('addMutation')
            },
            delCount ({ commit }) {
              commit('delMutation')
            },
            changeCount ({ dispatch }, { type } = { type: 1{})if (type === 1) {
                dispatch('addCount')}else {
                dispatch('delCount')}}},mutations: {
            addMutation (state) {
              console.log('addMutation1');
              state.count = state.count + 1
            },
            delMutation (state) {
              state.count = state.count - 1}}},// Inherits the parent module's namespace
        posts: {
          state: { popular: 2 },
          getters: {
            popular (state) {
              return state.popular
            }
          }
        }
      }
    }
  }
})
Copy the code

So this is for modules, and if modules are present, then forEachValue is called to traverse modules

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key= > fn(obj[key], key))
}
Copy the code

Register all modules that exist in Modules, where the key is the name of each module, such as profile and account in ExampleA

When we call this.register again, path.length === 0 is not true, so else logic is used, and we get this.get method:

// src/module/module-collection.js
get (path) {
  return path.reduce((module, key) = > {
    return module.getChild(key)
  }, this.root)
}
Copy the code

The path is iterated, and then the iterated item is called getChild. This getChild is a method in the previous Module class that gets submodule objects by key, by Module name, in the current Module. The corresponding method is addChild, Add a submodule to the current module, that is, establish the relationship between the parent and child:

// src/module/module.js
this._children = Object.create(null)
// ...
addChild (key, module) {
  this._children[key] = module
}
// ...
getChild (key) {
  return this._children[key]
}
Copy the code

The following operations are used to iterate over all Modules and their submodules using the module name as the property key to form a Modules Tree with this. Root as the vertex.

Install the module tree

With the Module tree built above, it’s time to install the tree

// src/store.js
const state = this._modules.root.state

installModule(this, state, [], this._modules.root)
Copy the code

I’m doing a bunch of things in this method, one by one

If you find that the current Module has the namespaced attribute and its value is true, you will register it in the Namespace map.

const namespace = store._modules.getNamespace(path)

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

The getNamespace method is a method on the ModuleCollection class that concatenates the full namespace of the current module according to its path

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

Call getNamespace to get the namespace name, then use the namespace name as the key, and cache the module object to the corresponding namespace as a value on store._modulesNamespaceMap. This is available via this.$store._modulesNamespaceMap, for example, for the example in ExampleA:

Next is a judgment logic, accord! isRoot && ! The hot condition is executed, where isRoot is defined at the beginning of the installModule method:

constisRoot = ! path.lengthCopy the code

Path is the state of the parent-child relationship maintained by the Module tree. If == 0, isRoot = true; if == 0, isRoot = true; if == 0, isRoot = true

InstallModule = []; path.length === 0

Set up the state

// src/store.js
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 called:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) = > state[key], state)
    : state
}
Copy the code

In this case, the state of the final submodule is actually found through layers of path.reduce

For example, for state under account/myCount, whose path is [‘account’, ‘myCount’], the global state structure is as follows:

{
  profile: {... },account: {
    isAdmin: true.isLogin: true.// This is the namespace of the submodule myCount
    myCount: {
    // This is the state of the submodule myCount
      count: 1
    },
    posts: {
      popular: 2}}}Copy the code

When the getNestedState method is called on the global state and path = [‘account’, ‘myCount’], it will end up with the state of /myCount:

{
  count: 1
}
Copy the code

After finding the state of the specific submodule, mount it to store._withCommit

Building the local Context

The makeLocalContext method is then executed:

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

A general description of what this method does is given in its comments:

/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */
function makeLocalContext (store, namespace, path) {
  // ...
}
Copy the code

Local Dispatch, commit, getter, state, and, if no namespace exists, mount it directly to the root Module

The namespace module is called dispatch, COMMIT, getters, and state. If the module uses a namespace, the namespace will be appended to the path automatically

For example, for dispath, if the current module has a namespace, the module’s dispatch method is called and the namespace is concatenated to type, and then the method on store is found based on the concatenated type and executed:

// 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) { type = namespace + typeif(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)
  }
Copy the code

For ExampleA code, for example, want to change the account/myCount count value, can be directly call global enclosing $store. Dispatch (‘ account/myCount/changeCount ‘), When type = 1, dispatch(‘addCount’) is executed. This dispatch is used to execute actions for account/myCount. Instead of addCount in root Module

Therefore, a full path type concatenation is performed here. The namespace of the current module is concatenated with type, that is, account/myCount/ and addCount are concatenated to account/myCount/addCount. We pass the full path type as an argument to the store.dispatch method, which simplifies the concatenation of nested Module paths

The logic for commit is similar, but the getter and state are a little different

// src/store.js
// makeLocalContext

Object.defineProperties(local, {
  getters: {
    get: noNamespace
      ? (a)= > store.getters
      : (a)= > makeLocalGetters(store, namespace)
  },
  state: {
    get: (a)= > getNestedState(store.state, path)
  }
})
Copy the code

For getters, if there is no namspace, return store.getters directly, otherwise makeLocalGetters is called:

// src/store.js
function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type= > {
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) ! == namespace)return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: (a)= > store.getters[type],
      enumerable: true})})return gettersProxy
}
Copy the code

It might not be very clear to look at this code directly, so here’s an example. For example, for the getter account/myCount/count (type in the source code above), its namespace is Account /myCount/, Its localType is count, and when accessing getters gettersproxy. count, it automatically points to the global account/myCount/count

And then state, which calls getNestedState, which is basically the same as the above method, but I won’t go into that

In addition, object.defineProperty is used several times to set the get function to attributes on the Object, rather than assigning values to attributes such as localType above. The purpose of this method is clearly commented in the code. In order to be able to calculate the value at the time of access, which not only reduces the real-time computation, but also ensures that the value obtained is real-time and accurate. This is related to the responsive mechanism of VUE, which will not be discussed here

The makeLocalContext method is a global dispatch, commit, getter, state mapping of a namespace submodule:

The vuex website introduces the Actions section with this quote:

Where the Action function takes a context object with the same methods and properties as the Store instance

Register mutation Action getters

Mutation

First, Mutation:

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

Mutations are mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations

// src/store.js
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

The method adds wrappedMutationHandler to _mutations[types] on the root store. The value of store._mutations[type] is an array, which means that _mutations of the same type can correspond to multiple wrappedMutationHandler methods

For example, for the account/myCount module in ExampleA, if its namespaced attribute doesn’t exist, or its value is false, that is, it doesn’t have a separate namespace, and then mutations has a method called goLogin, Mutations [‘account/goLogin’] have two entries in the array, one is the goLogin method under account, One is the goLogin method under Account /myCount

This is not the case if namespaced for Account /myCount, since its goLogin type is Account /myCount/goLogin

action

// src/store.js
module.forEachAction((action, key) = > {
  const type = action.root ? key : namespace + key
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})
Copy the code

The Mutation does not delete all actions from the store, but does not delete all actions from the store. Actions are stored on the store. It also has to do with namespaces

getter

// src/store.js
module.forEachGetter((getter, key) = > {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
Copy the code

Getters are the same logic, going through all getters and then hanging onto a store property, except that the getter is hanging on store._wrappedgetters. Additionally, only one value is allowed for the same key, and if multiple values exist, The second shall prevail:

// src/store.js
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)}}Copy the code

Finally, if the current module has submodules, it iterates through all of its submodules and executes the installModule method on those submodules, which repeats the above steps again

This completes the installModule method. When you call installModule, there are two lines of comment:

// src/store.js

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

It means:

Initializing root Modules also registers all child modules recursively and collects getters for all modules to this._wrappedgettersCopy the code

InstallModule is an initialization for state, getters, actions, mutations for all modules, including submodules

Initialize the Store VM

Next, resetStoreVM is executed:

// src/store.js

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

The function of this method can be seen roughly from its comments. Initialize the Store VM. When we look at this VM we should think of the vue instance VM

And registers _wrappedGetters as a computed property, which is a collection of getters for each module, as mentioned earlier, One of the features of computed properties in VUE is that computed properties are cached based on their dependencies. They are reevaluated only when the dependencies change, which means efficient real-time calculation. Here we want getters of each module on store to have this feature as well

// src/store.js
// resetStoreVM

store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
  // use computed to leverage its lazy-caching mechanism
  computed[key] = (a)= > fn(store)
  Object.defineProperty(store.getters, key, {
    get: (a)= > store._vm[key],
    enumerable: true // for local getters})})Copy the code

Run this through _wrappedGetters using forEachValue, which was also mentioned earlier, so fn(store) here is essentially this:

store._wrappedGetters[type] = function wrappedGetter (store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters)}Copy the code

WrappedGetter returns the result of a rawGetter. The rawGetter can be seen as the result of a getter’s calculation, so the four parameters we get in the parameters of the getter method refer to the above four:

// https://vuex.vuejs.org/zh/api/#getters

state,       // Local state of the module if defined in the module
getters,     // Equivalent to store.getters
rootState    // Equivalent to store.state
rootGetters  / / all getters
Copy the code

Once you get the getter, you give it to computed

We then define an Object.defineProperty:

/ / SRC, store. Js

Object.defineProperty(store.getters, key, {
  get: (a)= > store._vm[key],
  enumerable: true // for local getters
})
Copy the code

Store. getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; It has to do with this logic:

/ / SRC, store. Js

store._vm = new Vue({
  data: {
    ? state: state
  },
  computed
})
Copy the code

Store._vm is really just an instance of VUE that has only data and computed attributes, just to take advantage of vUE’s responsive mechanism

There is a mapping between state and getter, because the calculation result of getter must depend on state, and there must be a correlation between them. The Store class has a accessor property for state:

// src/store.js

get state () {
  return this._vm._data.? state }Copy the code

The state to getter mapping flow is as follows:

Here is a logic to standardize the development approach:

// enable strict mode for new vm
if (store.strict) {
  enableStrictMode(store)
}
Copy the code

Strict is explicitly declared when the developer initialses the store. It seems that most people don’t care about this, but in order to comply with vuex’s development specification, it is better to add this property

The enableStrictMode method is as follows:

// src/store.js

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

As mentioned above, store._vm is actually a vue instance, so it has a $watch method that checks this._data.? Case of state (” state “), store. _research must be true (” right “)

Research (” store. _research “) indicates the value defined in the store initialization code (” store “), which defaults to false:

// src/store.js

this._committing = false
Copy the code

This value is modified in the _withCommit method:

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

“Make sure this. __research is true (right) when fn is executed and reset (right),” this _withCommit scenario usually changes state (commit) :

// src/store.js

commit (_type, _payload, _options) {
  // omit extraneous code
  // ...
  this._withCommit((a)= > {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  // omit extraneous code
  // ...
}
Copy the code

EnableStrictMode is used to prevent the state value from being illegally modified by vuEX methods, such as commit and replaceState, from being warned in the development environment

conclusion

From the above analysis, the initialization of VUex is basically closely related to the initialization of Store. After the initialization of Store, vuex is basically initialized, but there are still many parts involved in the process

Up to now, all the analysis is about initialization. Vuex’s API has hardly been mentioned, and vuex’s ability is reflected through THE API. Let’s analyze vuex API again at some time