Vuex source code analysis

Understand vuex

What is a vuex



Vuex is a state manager for unified state management for VUE, which is mainly divided into state, getters, mutations and Actions.

Vue components are rendered based on state, and the re-rendering of components is triggered when state changes, and getters are derived based on the responsive principle of VUE.

Getters constructs different forms of data based on state. When state changes, it changes in response. The state of

Changes can only be triggered by a COMMIT, and each change is logged by DevTools. Asynchronous actions are triggered by actions, such as sending background API requests,

When the asynchronous operation is complete, the value is obtained and the mutations event is triggered, which in turn reevaluates the STAT and triggers the view to be rerendered.

Why vuex

  • To solve the communication between components and the traditional event mode is too long to debug the call chain. In the use of VUE, we use the event mode provided by VUE to realize the communication between father and son, or use eventBus to carry out multi-components

Between traffic, but as the project becomes huge, invocation chain sometimes is long, will be unable to locate to the sponsor of the event, and debugging is based on the pattern of events will let developers A headache, the next person to take over project is hard to know what are the effects to trigger an event, vuex state layer and view layer to pull away, All states are managed uniformly and all components share a single state. With Vuex we move our focus from events to data. We can only focus on which components refer to a value in a state. In addition, communication between components becomes much easier by moving from subscribing to the same event to sharing the same data.

  • To solve the problem of data transmission between parent and child components, in the development of VUE, we will use props or inject to realize data transmission between parent and child components, but when the component hierarchy is too deep

Some components that don’t need specific data inject unnecessary data for data delivery. The data delivery of inject is inherently flawed when the code is filled with provided and inject. Don’t know where the component Inject data is provided. Vuex takes some common data and manages it in a unified way, making this complex data transfer effortless.

The install

To implement introducing vuex through the vue.use () method, you need to define an install method for vuex. The main function of the Intall method in VUEX is to inject the Store instance into each VUE component as follows

Export function install (_Vue) {if (Vue && Vue === _Vue) {console. Warn ("duplicate install"); } Vue = _Vue; // Start registering global mixins applyMixin(Vue); }Copy the code

The above code avoids duplicate installation by defining a global variable Vue to hold the current imported Vue, and then injecting store into each instance through the apllyMixin implementation

Export default function (Vue) {const version = Number(Vue.version.split(".")[0]); If (version >= 2) {Vue. Mixin ({beforeCreate: vuexInit}); } else {// lower version, put the initialization method in options.init const _init = vue.prototype. _init; Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit; _init(); }; } function vuexInit () {// Root component if (this.$options && this.$options.store) {this.$store = typeof this.$options.store === "function" ? this.$options.store() : this.$options.store; } else if (this.$options.parent && this.$options.parent.$store) {// Non-root component this. }}}Copy the code

$options (new Vue(options:Object) when the root instance is instantiated)

new Vue({
  store
})Copy the code

Contains the store attribute, and if so, this of the instance.Options. store, if not, point to this.The store.

After installing, we can point all the child components’ $store property to this store by passing store to Options when instantiating the root component.

In addition, vueInit will be executed when applyMixin is executed to determine the current Vue version number. After version 2, vueInit will be executed when all components are instantiated by mixin

For versions 2 and below, the injection is performed by insertion in options.init. The following are some summary of the installation functions

  • Avoid repeated installation
  • The initial method is injected in different ways according to the version. The initial method is injected through options.init before 2 and mixin after 2
  • Inject store into the instance property $store of all vues

How to implement a simple COMMIT

Commit is actually a simple publish-subscribe implementation, but it involves module implementation, reactive implementation between state and getters, and lays the groundwork for actions later

use

First, review the use of COMMIT

Open mutations = new vuex. store ({state: {count: 1}, mutations: { add (state, number) { state.count += number; }}});Copy the code

When a store is instantiated, the mutation parameter is an event in the event queue. Each event is passed two parameters, namely, state and payload, and each event realizes changing the value of state according to the payload

<template> <div> count:{{state.count}} <button @click="add">add</button> </div> </template> <script> export default { name: "app", created () { console.log(this); }, computed: { state () { return this.$store.state; } }, methods: { add () { this.$store.commit("add", 2); }}}; </script> <style scoped> </style>Copy the code

We trigger the appropriate type of mutation in the component with a commit and pass in a payload, where the state changes in real time

implementation

First, what do we need to do in the constructor to implement commit

This._mutations = object.create (null); this._modules = new ModuleCollection(options); // Declare the publishing function const store = this; const { commit } = this; this.commit = function (_type, _payload, _options) { commit.call(store, _type, _payload, _options); }; const state = this._modules.root.state; // Install the root module this.installModule(this, state, [], this._modules. This.resetstorevm (this, state); }Copy the code

The first is the three instance properties _mutations is an event queue in publish subscribe mode, and the _modules property encapsulates the passed options:{state, getters, mutations, Actions} to provide some basic operation methods. The commit method fires the corresponding event in the event queue. We will then register the event queue in installModule and implement a responsive state in resetStoreVm.

modules

In instantiation store when we pass in an object parameter, which contains the state, mutations, the actions, getters, modules such as data items we need to encapsulate the data items, and exposed a this some of the operating method of a data item, this is the role of the Module class, In addition, vuEX has modules division, which needs to be managed, and thus derived ModuleCollection class. This section first focuses on the implementation of COMMIT, and module division will be discussed later. For the directly passed state, mutations, actions, Getters, in Vuex, are wrapped in the Module class and registered in the Root property of the ModuleCollection

export default class Module { constructor (rawModule, runtime) { const rawState = rawModule.state; this.runtime = runtime; // 1. Todo: This._rawModule = rawModule; this.state = typeof rawState === "function" ? rawState() : rawState; Mutations) {forEachValue(this._rawmodule. Mutations) {mutations (this._rawmodule. Mutations, fn); } } } export function forEachValue (obj, fn) { Object.keys(obj).forEach((key) => fn(obj[key], key)); }Copy the code

The rawModule parameters passed into the constructor are {state, mutations, actions, Getters} object, defined in the Module class two attributes _rawModule to hold the incoming rawModule, forEachMutation mutations traverse the execution, mutation object of value, the key to fn and perform, Next, attach this Module to the Root property of the Modulecollection

Export default class ModuleCollection {constructor (rawRootModule) {// path,module,runtime this.register([], rawRootModule, false); } // 1. What does todo Runtime do? register (path, rawRootModule, runtime) { const module = new Module(rawRootModule, runtime); this.root = module; }}Copy the code

After all this wrapping, the this._modules property is the following data structure



state

Since all events saved in mutations are to change state according to certain rules, we will first introduce how Store manages state, especially how to change the value of getters in response by changing state. One method mentioned in the constructor, resetStoreVm, implements the reactive relationship between state and getters

resetStoreVm (store, state) { const oldVm = store._vm; _vm = new Vue({data: {? state: state } }); If (oldVm) {vue.nexttick (() => {oldVm. Destroy (); }); }}Copy the code

This function takes two arguments, instance itself and state, and first registers a vue instance stored on the Store instance attribute _VM, where the data item is defined

export class Store {
  get state () {
    return this._vm._data.?state;
  }

  set state (v) {
    if (process.env.NODE_ENV !== "production") {
      console.error("user store.replaceState()");
    }
  }
}Copy the code

It is important to note that we cannot assign state directly, but rather through store.replacEstate, otherwise an error will be reported

Event registration

The publish-subscribe model consists of two steps: event subscription and event publishing. How does Vuex implement the subscription process

This._mutations = object.create (null); // Null this._modules = new ModuleCollection(options); const state = this._modules.root.state; // Install the root module this.installModule(this, state, [], this._modules. } installModule (store, state, path, module) {const local = this.makelocalContext (store, path); module.forEachMutation((mutation, key) => { this.registerMutation(store, key, mutation, local); }); // mutation registerMutation (store, type, handler, local) { const entry = this._mutations[type] || (this._mutations[type] = []); entry.push(function WrappedMutationHandler (payload) { handler.call(store, local.state, payload); }); }}Copy the code

We only intercept relevant parts of the code, including two key methods installModule and registerMutation. We will omit some parts about module encapsulation here. Local here can be simply understood as a {state, getters} object. The general process of event registration is to traverse mutation and wrap it and push it into the event queue of the specified type. The mutation is first traversed through forEachMutation, an instance method of the Moulde class. In addition, registerMutation is performed to register the event, and an event queue of the specified type of this._mutations is generated in registerMutation. The data structure of this._mutations after the registered event is as follows

Event publishing

Based on the structure of this._mutations after event registration, we can easily implement event release, find the event queue of the specified type, traverse the queue, pass in parameters, and execute them.

Const {type, payload} = unifyObjectStyle(_type, payload, _options) { _payload, _options); const entry = this._mutations[type]; ForEach (function commitIterator (handler) {handler(payload); }); }Copy the code

But it’s important to note that the parameters need to be handled first, which is what unifyObjectStyle does

// Add parameters: Function unifyObjectStyle (type, payload, options) {if (isObject(type)) {payload = type; options = payload; type = type.type; } return { type, payload, options }; }Copy the code

It can be either a string or an object, and when it’s an object, the internal type is type.type, and the second parameter becomes type, and the third parameter becomes payload. At this point, the principle of commit is covered, and all the code can be found at the branch github.com/miracle931….

Action and Dispatch principles

usage

Define an Action

add ({ commit }, number) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const pow = 2;
          commit("add", Math.pow(number, pow));
          resolve(number);
        }, 1000);
      });
    }Copy the code

Trigger action

 this.$store.dispatch("add", 4).then((data) => {
          console.log(data);
        });Copy the code

Why action is needed

Sometimes we need to trigger an event that executes asynchronously, such as an interface request, but if we rely on a synchronized event queue like Mutatoin, we can’t get the final state of the execution. At this point we need to find a solution to achieve the following two goals

  • A queue that executes asynchronously
  • Capture the final state of asynchronous execution

Through these two goals, we can roughly calculate how to achieve it. As long as we ensure that all events defined return a promise, and then put these promises in a queue and execute them through promise.all, a promise of final state will be returned, which can not only ensure the execution order of events, It can also capture the final execution state.

Implementation of Action and Dispatch

registered

First we define an instance property, _Actions, to hold the event queue

constructor (options = {}) {
    // ...
    this._actions = Object.create(null);
    // ...
  }Copy the code

We then define an instance method, forEachActions, in the Module class to iterate through the actions

export default class Module { // ... forEachAction (fn) { if (this._rawModule.actions) { forEachValue(this._rawModule.actions, fn); }} / /... }Copy the code

Then, during the installModule phase, go through the Actions and register the event queue

installModule (store, state, path, module) {
    // ...
    module.forEachAction((action, key) => {
      this.registerAction(store, key, action, local);
    });
    // ...
  }Copy the code

registered

registerAction (store, type, handler, local) { const entry = this._actions[type] || (this._actions[type] = []); entry.push(function WrappedActionHandler (payload, cb) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, state: local.state, rootState: store.state }, payload, cb); // The default action returns a promise. If not, wrap the return value in the promise if (! isPromise(res)) { res = Promise.resolve(res); } return res; }); }Copy the code

The registration method contains four parameters: store for store instance, type for Action type, and handler for action function. First check whether there is an event queue of this type acion. If not, initialize it to an array. The event is then pushed to the event queue of the specified type. Two things to note: first, the action function calls a context object as its first argument, and second, the event always returns a promise.

release

dispatch (_type, _payload) { const { type, payload } = unifyObjectStyle(_type, _payload); / /?? Action const entry = this._actions[type]; action const entry = this._actions[type]; // Return promise,dispatch().then() accepts an array or some value return entry.length > 1? Promise.all(entry.map((handler) => handler(payload))) : entry[0](payload); }Copy the code

First get the event queue of the corresponding type, then pass in the parameter execution, return a promise, when the number of events contained in the event queue is greater than 1, save the returned promise in an array, and then trigger through pomise.all, If there is only one event in the event queue, then we can get the result of asynchronous execution via dispatch(type, payload). Then (data=>{}). In the event queue, events are triggered by promise.all. Both objectives have been achieved.

Getters principle

The use of the getters

In store instantiation we define the following options:

const store = new Vuex.Store({ state: { count: 1 }, getters: { square (state, getters) { return Math.pow(state.count, 2); } }, mutations: { add (state, number) { state.count += number; }}});Copy the code

First, we define a state, getters, and mutations in store, where state contains a count with an initial value of 1, getters defines a square that returns the square of count, and mutations defines an add event, Count increases the number when add is triggered. Then we use the store in the page:

<template> <div> <div>count:{{state.count}}</div> <div>getterCount:{{getters.square}}</div> <button @click="add">add</button> </div> </template> <script> export default { name: "app", created () { console.log(this); }, computed: { state () { return this.$store.state; }, getters () { return this.$store.getters; } }, methods: { add () { this.$store.commit("add", 2); }}}; </script> <style scoped> </style>Copy the code

The result of this execution is that every time we fire the Add event, state.count increases by 2, and the getter always squares state.count. This reminds us of the relationship between data and computed in VUE, which vuex uses in fact.

The realization of the getters

Start by defining an instance property _wappedGetters to store getters

export class Store { constructor (options = {}) { // ... this._wrappedGetters = Object.create(null); / /... }}Copy the code

Define an instance method to iterate over getters in Modules, register getters in the installModule method, and store it in the _wrappedGetters property

installModule (store, state, path, module) {
    // ...
    module.forEachGetters((getter, key) => {
      this.registerGetter(store, key, getter, local);
    });
    // ...
  }Copy the code
registerGetter (store, type, rawGetters, Local) {// Handle getter duplicates if (this._wrappedgetters [type]) {console.error("duplicate getter"); } // set _wrappedGetters, This._wrappedgetters [type] = function wrappedGetterHandlers (store) {return rawGetters(local.state, local.getters, store.state, store.getters ); }; }Copy the code

Note that vuex cannot define two getters of the same type. At registration time, we pass in a function that returns the result of the execution of the option getters as a store instance. Getters in the option accepts four parameters: state and getters in scope and store instance. The problem of local will be introduced later in module principle. In this implementation, the parameters of local and store are consistent. Then we need to inject all getters into computed during resetStoreVm, and when we access an attribute in getters, we need to delegate it to the corresponding attribute in store.vm

// Register a responsive instance resetStoreVm (store, state) {// Point store.getters[key] to store._vm[key],computed assignment forEachValue(wrappedGetters, function (fn, key) { computed[key] = () => fn(store); }); _vm = new Vue({data: {? state: state }, computed }); If (oldVm) {vue.nexttick (() => {oldVm. Destroy (); }); }}Copy the code

During the resetStroreVm period, walk through wrappedGetters, wrap getters in a computed with the same key, and inject this computed into the store._VM instance.

resetStoreVm (store, state) {
    store.getters = {};
    forEachValue(wrappedGetters, function (fn, key) {
    // ...
      Object.defineProperty(store.getters, key, {
        get: () => store._vm[key],
        enumerable: true
      });
    });
    // ...
  }Copy the code

Then point the properties in store.getters to the corresponding properties in store._vm, which is store.puted so that when the data.? State (store.state) in store._vm changes, Getters that refer to state are also evaluated in real time. That’s how getters respond to changes. See github.com/miracle931.

Principle of helpers

Helpers. Js exposed the outward in the four methods, respectively mapState, mapGetters, mapMutations and mapAction. These four helper methods help developers quickly reference their own defined state,getters,mutations, and actions in their components. Learn how to use it first and then how it works

const store = new Vuex.Store({ state: { count: 1 }, getters: { square (state, getters) { return Math.pow(state.count, 2); } }, mutations: { add (state, number) { state.count += number; } }, actions: { add ({ commit }, number) { return new Promise((resolve, reject) => { setTimeout(() => { const pow = 2; commit("add", Math.pow(number, pow)); resolve(number); }, 1000); }); }}});Copy the code

So that’s our definition of store

<template> <div> <div>count:{{count}}</div> <div>getterCount:{{square}}</div> <button @click="mutAdd(1)">mutAdd</button>  <button @click="actAdd(1)">actAdd</button> </div> </template> <script> import vuex from "./vuex/src"; export default { name: "app", computed: { ... vuex.mapState(["count"]), ... vuex.mapGetters(["square"]) }, methods: { ... vuex.mapMutations({ mutAdd: "add" }), ... vuex.mapActions({ actAdd: "add" }) } }; </script> <style scoped> </style>Copy the code

Store is then introduced into the component and used by mapXXX. By looking at the way these methods are referenced, you can see that each of these methods eventually returns an object whose values are all functions, and that these methods are injected into the computed and methods properties by expanding the operator. For mapState and mapGetters, return a function in an object that returns the value of the passed argument (return store.state[key]; Or return store.getters[key]), and for mapMutations and mapActions, return a function in an object, which executes commit ([key],payload), Or dispatch ([key],payload). This is the simple principle of these methods. We will look at vuEX implementations one by one

MapState and mapGetters

Export const mapState = function (states) {// define a result map const res = {}; State normalizeMap(states).foreach (({key, Function mappedState () {const state = this.$store. State; const getters = this.$store.getters; return typeof val === "function" ? val.call(this, state, getters) : state[val]; }; }); Return res; };Copy the code

First, the final return value of mapsState is an object, and the parameters we pass in are the properties that we want to map out. MapState can be passed in either an array of strings that contain the referenced properties, or an array of objects that contain the mapping between values and references. These two forms of parameter passing, We need to normalize with normalizeMap to return a uniform array of objects

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

The normalizeMap function first checks whether the value passed in is an array. If it is, it returns an array of objects in which both key and val are array elements. If it is not an array, the normalizeMap function determines that the value passed in is an object. After normalizeMap, the map will be an array of objects. It then iterates through the normalized array and assigns to the returned object. The assignment function returns the key corresponding to state. If the value passed in is a function, getters and state are passed in and executed, and the object is returned. This allows you to reference the value of state directly through key when expanded in computed properties. The implementation principle of mapGetters and mapState is basically the same

export const mapGetters = function (getters) {
  const res = {};
  normalizeMap(getters)
    .forEach(({ key, val }) => {
      res[key] = function mappedGetter () {
        return this.$store.getters[val];
      };
    });

  return res;
};Copy the code

MapActions and mapMutations

export const mapActions = function (actions) { const res = {}; normalizeMap(actions) .forEach(({ key, val }) => { res[key] = function (... args) { const dispatch = this.$store.dispatch; return typeof val === "function" ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this, [val].concat(args)); }; }); return res; };Copy the code

MapActions also returns an object whose key is referenced in the component and whose value is a function that takes the payload during the execution of dispatch. The action is triggered by dispath(actionType,payload). If the parameter is a function, the dispatch and payload are passed as parameters and executed. This makes it possible to call multiple actions in combination with mapActions, or to customize some other behavior. This object is eventually returned, and when expanded in the component’s Methods property, the action can be triggered by calling the function corresponding to the key. MapMutation is implemented in much the same way as mapActions

export const mapMutations = function (mutations) { const res = {}; normalizeMap(mutations) .forEach(({ key, val }) => { res[key] = function mappedMutation (... args) { const commit = this.$store.commit; return typeof val === "function" ? val.apply(this, [commit].concat(args)) : commit.apply(this, [val].concat(args)); }; }); return res; };Copy the code

module

In order to facilitate the segmentation of different functions in Store, different functions can be assembled into a separate module in VUEX. State can be separately managed inside the module and global state can be accessed.

usage

// main.js const store = new Vuex.Store({ state: {}, getters: {}, mutations: {}, actions: {}, modules: { a: { namespaced: true, state: { countA: 9 }, getters: { sqrt (state) { return Math.sqrt(state.countA); } }, mutations: { miner (state, payload) { state.countA -= payload; } }, actions: { miner (context) { console.log(context); }}}}});Copy the code
//app.vue <template> <div> <div>moduleSqrt:{{sqrt}}</div> <div>moduleCount:{{countA}}</div> <button @click="miner(1)">modMutAdd</button> </div> </template> <script> import vuex from "./vuex/src"; export default { name: "app", created () { console.log(this.$store); }, computed: { ... vuex.mapGetters("a", ["sqrt"]), ... vuex.mapState("a", ["countA"]) }, methods: { ... vuex.mapMutations("a", ["miner"]) } }; </script> <style scoped> </style>Copy the code

In the code above, we define a module with key A, whose namespaced is true, and for modules with namespace=false, it automatically inherits the parent module’s namespace. For module A, it has the following features

  • Have your own separate state
  • Getters and Actions can access state,getters, rootState, rootGetters
  • Mutations can only change the state in the module

Based on the above features, the subsequent Module implementation can be divided into several parts

  • What data format will be used to store the Module
  • How do I create a module context that encapsulates state, commit, dispatch, and getters, and make the commit change only the internal state, and make the module’s context change only the internal state

Getters, Dispatch keeps the root module accessible

  • How to register getters, mutations, actions in the module and bind them to namespace
  • How do helper methods find getters, Mutations, and actions in a namespace and inject them into the component

Construct a nested module structure

The final module constructed by VUex is such a nested structure





The first level is a root, and each level after that has an _rawModule and _children property that holds its own getters, Mutations, actions and, respectively

The child. Implementing such a data structure can be done with a simple recursion

The first is our input parameter, which looks something like this

{
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    a: {
      namespaced: true,
      state: {},
      getters: {},
      mutations: {},
      actions: {}
    },
    b: {
        namespaced: true,
        state: {},
        getters: {},
        mutations: {},
        actions: {}
    }
  }
}Copy the code

We will use this object as an argument to the ModuleCollection instantiation in the store constructor

export class Store { constructor (options = {}) { this._modules = new ModuleCollection(options); }}Copy the code

All the construction of nested structures takes place during the Instantiation of the ModuleCollection

// module-collection.js export default class ModuleCollection {constructor (rawRootModule) { path,module,runtime this.register([], rawRootModule, false); } get (path) {return path.reduce((module, key) => module.getChild(key), this.root); } // 1. What does todo Runtime do? Register (path, rawModule, Runtime = true) {// Generate Module const newModule = newModule (rawModule, Runtime); If (path.length === 0) {// Root module, register on root this.root = newModule; } else {// Attach const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // Whether the module contains submodules, If (rawModule.modules) {forEachValue(rawModule.modules, (newRawModule, key) => { this.register(path.concat(key), newRawModule, runtime); }); }}}Copy the code
// module.js export default class Module { addChild (key, module) { this._children[key] = module; }}Copy the code

In the register function, an instance of a Module is created based on the rawModule passed in. Then, the registered path is used to determine whether the Module is the root Module. If so, the Module instance is mounted to the root attribute. If not, find the parent module of the module through get method, mount it to the _children attribute of the parent module through addChild method of the module, and finally determine whether the module contains nested modules. If so, traverse nested modules and recursively execute register method. This allows you to construct the nested module structure shown above. With the above structure, we can use reduce method to obtain the modules under the specified path through path, and we can also use recursive way to carry out unified operation for all modules, which greatly facilitates module management.

Tectonic localContext

With the basic module structure in place, the next question is how to encapsulate the module scope so that each module has its own state and methods for managing that state, and we want those methods to have access to global properties as well. To sum up what we’re going to do now,

// module
{
    state: {},
    getters: {}
    ...
    modules:{
        n1:{
            namespaced: true,
            getters: {
                g(state, rootState) {
                    state.s // => state.n1.s
                    rootState.s // => state.s
                }
            },
            mutations: {
                m(state) {
                    state.s // => state.n1.s
                }
            },
            actions: {
                a({state, getters, commit, dispatch}) {
                    commit("m"); // => mutations["n1/m"]
                    dispatch("a1"); // => actions["n1/a1"]
                    getters.g // => getters["n1/g"]
                },
                a1(){}
            }
        }
    }
}Copy the code

In a module where namespaced=true, the accessed state and getters are from the module’s internal state and getters, and only rootState and rootGetters point to the root module’s state and getters. In addition, in the module, commit triggers mutations within the submodule, while dispatch triggers actions within the submodule. This encapsulation is implemented in VUEX through path matching.

//state
{
   "s": "any"
   "n1": {
       "s": "any",
       "n2": {
           "s": "any"
       }
   } 
}
// getters
{
  "g": function () {},
  "n1/g": function () {},
  "n1/n2/g": function () {}
}
// mutations
{
    "m": function () {},
    "n1/m": function () {},
    "n1/n2/m": function () {}
}
// actions
{
  "a": function () {},
  "n1/a": function () {},
  "n1/n2/a": function () {}
}Copy the code

In VUEX, we need to construct such a data structure to store each data item, and then rewrite the commit method in the context, and delegate commit(type) to namespaceType to realize the encapsulation of commit method. Similar dispatches are also encapsulated in this way. Getters implements a getterProxy, proxies key to store.getters[namespace+key], and then replaces getters in context with getterProxy. State uses the above data structure. Find the corresponding path state and assign it to context.state, so that all data accessed through the context is inside the module. Now let’s look at the code implementation

installModule (store, state, path, module, hot) { const isRoot = ! path.length; // getNamespace const namespace = store._modules.getnamespace (path); }Copy the code

The construction of all data items, as well as the construction of the context, is in the installModule method of store.js, which first gets the namespace through the path passed in

GetNamespace (path) {let Module = this.root; return path.reduce((namespace, key) => { module = module.getChild(key); return namespace + (module.namespaced ? `${key}/` : ""); }, ""); }Copy the code

The method that gets namespace is an instance method of ModuleCollections, which accesses Modules layer by layer and checks for namespaced properties, If true, the path[index] is spelled on the namespace so that the namespace is complete followed by the implementation of the nested structure state

InstallModule (Store, state, path, module, hot) {// Construct nested state if (! isRoot && ! hot) { const moduleName = path[path.length - 1]; const parentState = getNestedState(state, path.slice(0, -1)); Vue.set(parentState, moduleName, module.state); }}Copy the code

Obtain the parentState corresponding to the state according to the path, where the input parameter state is store.state

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

Where getNestState is used to obtain the corresponding state according to the path. After obtaining parentState, mount module.state on parentState[moduleName]. This creates a nested state structure as described above. After obtaining the namespace, we need to construct the passed getters, mutations, and actions according to the namespace

installModule (store, state, path, module, hot) {
    module.forEachMutation((mutation, key) => {
      const namespacdType = namespace + key;
      this.registerMutation(store, namespacdType, mutation, local);
    });

    module.forEachAction((action, key) => {
      const type = action.root ? type : namespace + key;
      const handler = action.handler || action;
      this.registerAction(store, type, handler, local);
    });

    module.forEachGetters((getter, key) => {
      const namespacedType = namespace + key
      this.registerGetter(store, namespacedType, getter, local);
    });
  }Copy the code

The construction of getters, mutations and actions has almost the same way, but it is mounted on store._getters,store._mutations,stors._actions respectively. Therefore, we need to analyze the construction process of mutations. The mutations object in the Module is first traversed by forEachMutation, and then registered on the key with namespace+key through ergisterMustions

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

It’s actually stored on store._mutations[namespace+key]. Now that we have completed half of the encapsulation, we need to implement a context for each module that contains state, getters,commit, and actions. But state,getters can only access state and getters in the Module, commit and actions can only access state and getters in the Module

installModule (store, state, path, module, Hot) {// register the mutation event queue const local = module.context = makeLocalContext(store, namespace, path); }Copy the code

We’ll implement the context in installModule and assign the assembled context to local and module.context, respectively. The local will be passed to getters,mutations, and Actions as parameters in the register

function makeLocalContext (store, namespace, path) { const noNamespace = namespace === ""; const local = { dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); let { type } = args; const { payload, options } = args; if (! options || ! options.root) { type = namespace + type; } store.dispatch(type, payload, options); }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); let { type } = args; const { payload, options } = args; if (! options || ! options.root) { type = namespace + type; } store.commit(type, payload, options); }}; return local; }Copy the code

The commit and dispatch methods in context are implemented in the same way. We only analyze the COMMIT, and first determine whether the module is encapsulated by namespace. If so, an anonymous function is returned. A call to store.dispatch will sneak in the incoming type to namespace+type, So the commit[type] we perform on the encapsulated module is actually an event queue that calls store._mutations[namespace+type]

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === "";
  Object.defineProperties(local, {
    state: {
      get: () => getNestedState(store.state, path)
    },
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    }
  });

  return local;
}Copy the code

Then there is state, which is accessed by passing path to getNestedState, which is actually the state in the Module, while getters accesses internal Getters by proxy

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: () => store.getters[type], enumerable: true }) }) return gettersProxy }Copy the code

First declare a proxy object gettersProxy, and then go through store.getters to check whether the path of namespace is fully matched. If so, proxy localType property of gettersProxy to Store. getters[type]. Then return gettersProxy so that localType accessed via local.getters is actually stores. Getters [namespace+type]. Here’s a quick summary of how to get the path’s corresponding namespace (namespaced=true) ->state concatenates to store.state to make it a nested path-based structure -> Register localContext Register localContext

  • Dispatch: Namespace -> Flattening parameters -> No root condition triggers the namespace directly +type-> Root or hot condition triggers the type
  • Commit -> flat parameter -> No root condition triggers namespace+type-> Root or hot condition triggers namespace type
  • State: Searches for State based on path
  • Getters: Declares the proxy object, iterates over the Store. Getters object, matches the key and namespace, and points its localType to the full path