Vuex profile

Most of you reading this article have already used Vuex and would like to know more about the internal implementation of Vuex. So the introduction is less. The website states that Vuex is a state management model developed specifically for vue.js applications. It uses centralized storage to manage the state of all components of an application and rules to ensure that the state changes in a predictable way. The state of the data stream is clear, and the render function is triggered to update the view according to the data within the component dispatch Action -> Action commit Mutation -> Mutation and mutate state. Attached is a flow chart of the official website and vuEX’s official website address: vuex.vuejs.org/zh/

Questions

When using VUex, do you have the following questions? With these questions, look at the source code and find the solution, so that you can deepen your understanding of VUex.

  1. In strict mode, an error will be thrown whenever a state change occurs that is not caused by a mutation function. How does VUEX detect that a state change is caused by a mutation function?
  2. By registering the Store option in the root instance, the store instance is injected into all the children under the root component. Why can all child components be fetched to store?
  3. Why should the attributes used be defined in state before the VUE view can respond?
  4. When calling dispatch and commit, you only need to pass in (type, payload). Why can the action and mutation functions deconstruct state, commit, and so on in the first argument? With these questions in mind, let’s take a look at vuex’s source code to find the answers.

Source directory structure

Vuex’s source code structure is very simple and clear, and the amount of code is not very large, so don’t panic.

Vuex mount

Use (Plugins) for vue. For vuex, just use(vuex). How is plug-in registration implemented internally in USE? As anyone who has read the vue source code knows, the plugin’s Install method is called if the passed argument has an install method, and if the passed argument is itself a function, it is executed directly. Then we need to go to vuex’s exposed install method to see what’s going on.

store.js

export functionInstall (_Vue) {// Vue. use principle: The install method of a plug-in is called to register the plug-in, and the vue object is passed to the install method as the first parameterif (Vue && _Vue === Vue) {
    if(process.env.NODE_ENV ! = ="production") {
      console.error(
        "[vuex] already installed. Vue.use(Vuex) should be called only once."
      );
    }
    return; } Vue = _Vue; // To reference vue's watch method applyMixin(vue); }Copy the code

In Install, the vue object is assigned to the global variable vue and passed as a parameter to the applyMixin method. So what do we do in the applyMixin method?

mixin.js

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

The vuexInit function is added to the vuex beforeCreate hook function. In vuexInit, the store passed in when new Vue() is set to the $store property of this object, and the child component is nested by referencing its $store property from its parent. Ensure that the store object can be fetched from each component via this.$store. So that answers our question in question 2. By registering the Store option in the root instance, the store instance is injected into all the children of the root component by taking the child from the parent and the root from options.

Let’s see what new vuex.store () does.

Store constructor

The main code for store object construction is in Store.js, which is the core code of Vuex.

First, Vue is judged from constructor. If Vuex is not registered via vue. use(Vuex), install is called to register. (Vue.use(Vuex) does not need to be manually called when introduced via script tags) and determines in non-production environments: Vue.use(Vuex) must be called for registration, Promise must be supported, store must be created with new.

if(! Vue && typeof window ! = ="undefined" && window.Vue) {
    install(window.Vue);
}

if(process.env.NODE_ENV ! = ="production") { 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

A series of attributes are then initialized. The focus is on new ModuleCollection(Options), which we will cover later. Finish the constructor code first.

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

// store internal state
this._committing = false; // Whether the mutation status identifies this._actions = object.create (null); _actionSubscribers = []; // Save action, _Actions already wrapped this._actionSubscribers = []; This._mutations = object.create (null); // save mutations, _mutations' function is already wrapped this._wrappedGetters = object.create (null); // Vuex supports store as a Module, internally using the Module constructor to construct a Module object from the options passed in, // if there is no named Module, // ModuleCollection internally calls the new Module constructor this._modules = new ModuleCollection(options); this._modulesNamespaceMap = Object.create(null); // Module namespace map this._subscribers = []; // mutation subscription function set this._watcherVM = new Vue(); // The Vue component is used by the watch to monitor changesCopy the code

Once the property is initialized, the prototype dispatch and commit methods are first deconstructed from this and rewrapped to point this to the current store.

const store = this; const { dispatch, commit } = this; /** Place this pointer to the Store class's Dispatch and commit methods on the current Store instance. The purpose of this is to ensure that when we pass this in the component.$storeWhen you call the Dispatch /commit method directly, you can make this in the Dispatch/Commit method refer to the current Store object instead of this. */ this.dispatch = of the current componentfunction 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

We then move on to strict mode setting, root state assignment, module registration, state responsiveness, plug-in registration, etc. The focus is on the installModule function, which registers all modules.

// Whether strict mode is enabled when passed in options this.strict = strict; // new ModuleCollection constructs _mudules const state = this._modules.root.state; InstallModule (this, state, [], this._modules.root); // Initialize the root component, register all child components, and store all getters in this. // Initialize store._vm to make state responsible and change getters to calculate property resetStoreVM(this, state) by using vue instance; Plugins. forEach(plugin => plugin(this)); Const useDevtools = options.devtools! == undefined ? options.devtools : Vue.config.devtools;if (useDevtools) {
   devtoolPlugin(this);
}
Copy the code

At this point, all of the code in Constructor has been analyzed. The focus is on New ModuleCollection(Options) and installModule, so let’s take a look inside them and see what’s going on.

ModuleCollection

Because Vuex uses a single state tree, all the states of an application are grouped into one large object. When the application becomes very complex, the Store object can become quite bloated. Vuex allows us to split the Store into modules, each with its own state, mutation, action, getter, and even nested submodules. For example:

const childModule = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    childModule: childModule,
  }
})

Copy the code

With the concept of modules, we can better plan our code. For the data common to each module, we can define a common Store. If used by other modules, it can be directly introduced through the method of modules. There is no need to write the same code in each module repeatedly. ChildModule: store. State. ChildModule: Store. State.

exportDefault class ModuleCollection {constructor(rawRootModule) {// Options this.register([], rawRootModule,false);
  }

  register(path, rawModule, runtime = true) {
    if(process.env.NODE_ENV ! = ="production") {
      assertRawModule(path, rawModule);
    }

    const newModule = new Module(rawModule, runtime);
    if(path.length === 0) {// Register root Module this.root = newModule; }elseConst parent = this.get(path.slice(0, -1)); const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // If the current module has child modules, loop registrationif (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime); }); }}}Copy the code

The Module constructor is called in the ModuleCollection to construct a Module.

Module constructor

Constructor (rawModule, Runtime) {// When initializedfalseThis._children = object. create(null) // Store the original module, This._rawModule = rawModule const rawState = rawModule.state // Stores the state of the original module = = ='function' ? rawState() : rawState) || {}
  }
Copy the code

ModuleCollection constructs the entire options object as a Module object. This. Register ([key], rawModule, false) is looped to register modules properties as Module objects. Finally, the Options object is constructed as a complete Module tree.

The tree structure constructed by ModuleCollection is as follows :(the tree structure generated by the above example)

Once the module is created, the next thing to do is installModule.

installModule

Let’s first look at the structure of the tree after executing the installModule function in constructor.

As you can see from the above figure, after executing the installModule function, the state property in each module is added to the state property in its children, but the state is not yet responsive, and the context object is added. It contains functions like Dispatch and COMMIT, as well as properties like state and getters. It’s what vuex says in the official documentation that the Action function takes a Context object that has the same methods and properties as the Store instance. The dispatches and commit we call in the store are deconstructed from here. Let’s take a look at what happens in installModule.

functionInstallModule (Store, rootState, Path, module, hot) {// Check whether it is the root node, path = [] const isRoot =! path.length; // Take the namespace, similar form'childModule/'const namespace = store._modules.getNamespace(path); // If namespaced is usedtrue, stored in _modulesNamespaceMapif(module.namespaced) { store._modulesNamespaceMap[namespace] = module; } // Instead of the root node, set each state of the child component to its parent's state propertyif(! isRoot && ! State const parentState = getNestedState(rootState, path.slice(0, -1)); // Get the name of the current Module const moduleName = path[path.length-1]; store._withCommit(() => { Vue.set(parentState, moduleName, module.state); }); } // Assign a const to the contextlocal= (module.context = makeLocalContext(store, namespace, path)); ForEachMutation ((Mutation, key) => {const namespacedType = namespace + key; registerMutation(store, namespacedType, mutation,local); }); ForEachAction ((Action, key) => {consttype = action.root ? key : namespace + key;
    const handler = action.handler || action;
    registerAction(store, type, handler, local); }); // Loop to register the Getter module for each module. ForEachGetter ((Getter, key) => {const namespacedType = namespace + key; registerGetter(store, namespacedType, getter,local); }); // loop _childern attribute module.forEachChild((child, key) => {installModule(store, rootState, path.concat(key), child, hot); }); }Copy the code

In the installModule function, you first determine if it is the root node and if the namespace is set. With the namespace set, store the Module in store._modulesNamespaceMap. Get the state of the parent by getNestedState and the name of the current module. Mount the state of the current module to the parent state by vue.set (). Module. context is then assigned by calling makeLocalContext, setting the local dispatch and commit methods, as well as getters and state. So let’s look at this function.

functionMakeLocalContext (store, namespace, path) {// Whether a namespace exists const noNamespace = namespace ==="";

  const local= {// If there is no namespace, return store.dispatch directly; Otherwise, totypePlus the namespace, like that'childModule/'This dispatch: noNamespace? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args;let { type } = args;

          if(! options || ! options.root) {type = namespace + type;
            if( 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); }, // If there is no namespace, just return store.mit; Otherwise, totypeCommit: noNamespace? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); const { payload, options } = args;let { type } = args;

          if(! options || ! options.root) {type = namespace + type;
            if( process.env.NODE_ENV ! = ="production" &&
              !store._mutations[type]
            ) {
              console.error(
                `[vuex] unknown local mutation type: ${
                  args.type
                }, global type: ${type}`);return;
            }
          }

          store.commit(type, payload, options); }}; // getters and state object must be gotten lazily // because they will be changed by vm update Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  });

  return local;
}
Copy the code

The returned value processed by makeLocalContext is assigned to the local variable, which is passed to registerMutation, forEachAction, and registerGetter for appropriate registration.

Mutation can be registered repeatedly. The registerMutation function wraps the mutation we passed in once, passing state as the first parameter, so we can get the current state value from the first parameter when we call mutation.

function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push(functionWrappedMutationHandler (payload) {// Point this to the store, take the state of the makeLocalContext as the first argument, So when we call commit to commit the mutation, we can get the current state value from the first parameter of the mutation. handler.call(store, local.state, payload); }); }Copy the code

Actions can also be registered repeatedly. The method for registering an action is similar to mutation, and the registerAction function also wraps the action we passed in once. The actions will have more parameters, including dispatch, commit, local.getters, local.state, rootGetters, rootState, Therefore, you can dispatch another action in an action or commit a mutation. This answers the question raised in Question 4.

function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(functionWrappedActionHandler (payload, cb) {// The first argument to the action is an object, There are dispatch, commit, getters, state, rootGetters, rootStatelet res = handler.call(
      store,
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state
      },
      payload,
      cb
    );
    if(! isPromise(res)) { res = Promise.resolve(res); }if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit("vuex:error", err);
        throw err;
      });
    } else {
      returnres; }}); }Copy the code

Register getters. Local state, local getters, root state, root getters can be obtained from the first parameter of getters. Getters does not allow double registrations.

function registerGetter(store, type, rawGetter, local) {// getters does not allow repetitionif (store._wrappedGetters[type]) {
    if(process.env.NODE_ENV ! = ="production") {
      console.error(`[vuex] duplicate getter key: ${type}`);
    }
    return;
  }

  store._wrappedGetters[type] = functionWrappedGetter (store) {// the first argument to getters containslocalThe state,localGetters, root state, root gettersreturn rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    );
  };
}
Copy the code

We now have our own mutation and action functions in the store (_mutation and _action), which have gone through a layer of internal notification. How does this.$store.dispatch() and this.store.mit () call the corresponding function when executing this.$store.dispatch() and this.code.store.mit () in the component? Let’s take a look at the Dispatch and commit functions on the store.

commit

The commit function performs parameter adaptation first, and then determines whether the current action type exists. If so, the _withCommit function is called to perform the corresponding mutation.

// Commit the mutation function commit(_type, _payload, _options) {// check object-style commit'getName'.'vuex'), and the other is commit({type:'getName',name:'vuex'}), //unifyObjectStyle fits both ways const {type, payload, options } = unifyObjectStyle(
      _type,
      _payload,
      _options
    );

    const mutation = { type, payload }; // The entry value here is the function that we pushed into _mutations in the registerMutation function, which has been processed const entry = this._mutations[type];
    if(! entry) {if(process.env.NODE_ENV ! = ="production") {
        console.error(`[vuex] unknown mutation type: ${type}`);
      }
      return; } // Do not enable strict mode in a publishing environment whenever a state change occurs that is not caused by the mutation function. Strict mode deeply monitors the status tree to detect non-compliant state changes -- be sure to turn off Strict mode in a release environment to avoid performance losses. this._withCommit(() => { entry.forEach(functioncommitIterator(handler) { handler(payload); }); }); ForEach (sub => sub(mutation, this.state));if(process.env.NODE_ENV ! = ="production" && 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 _withCommit function is called in the commit function as follows. _withCommit is a proxy method through which all mutation triggering state changes are performed to centrally manage and monitor state changes. In strict mode, state changes are deeply monitored and an error is reported if the state is not modified by mutation. It is recommended not to enable strict mode in a release environment! Be sure to turn off strict mode in a release environment to avoid performance losses. That’s the answer to question 1.

_withCommit(fn) {// Save the previous commit statusfalseconst committing = this._committing; // Commit this time, if not set totrue", modify state directly, in strict mode, Vuex will generate a warning "this._research =" warning of illegal state modificationtrue; // Modify state fn(); // Restore the status before the modificationfalse
    this._committing = committing;
}
Copy the code

dispatch

Dispatch and COMMIT work in the same way. If there are multiple actions with the same name, the returned Promise will not be executed until all action functions have completed.

// Dispatch (_type, _payload) {// Check object-style dispatch const {type, payload } = unifyObjectStyle(_type, _payload);

    const action = { type, payload };
    const entry = this._actions[type];
    if(! entry) {if(process.env.NODE_ENV ! = ="production") {
        console.error(`[vuex] unknown action type: ${type}`);
      }
      return; } / / perform all subscribers function enclosing _actionSubscribers. ForEach (sub = > sub (action, this state));return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload);
  }
Copy the code

This completes the analysis of the entire installModule. Now let’s look at the Store tree structure.

The action and mutation we passed in in options are already in store. But State and Getters have not. That’s what the next resetStoreVM method does.

resetStoreVM

The resetStoreVM function initializes store._vm, watches for changes in state and getters, and executes whether strict mode is on. The state property is assigned to the vUE instance’s data property, so the data is responsive. This answers question 3: The attributes used must also be defined in state before the VUE view can respond.

functionResetStoreVM (store, state, hot) {const oldVm = store._vm; // Initialize store getters store. Getters = {}; // _wrappedGetters is const wrappedGetters = store._wrappedgetters; const computed = {};forEachValue(wrappedGetters, (fn, key) => {// To put getters into computed properties, you need to pass store into computed[key] = () => fn(store); // In order to pass this.$storeItem.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// Some funky global mixins // Use a vUE instance to store the store tree, pass getters as a calculated attribute, access this.$store_vm[XXX] const silent = vue.config. silent; Vue.config.silent =true;
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  });
  Vue.config.silent = silent;

  // enable strict mode forNew VM // If in strict mode, enable strict mode, deep watch state propertyif (store.strict) {
    enableStrictMode(store); } // If oldVm exists, unreference state and wait for DOM update to destroy the old vue instanceif (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

Deep listening when strict mode is on? Case of the state change, if it’s not a state change triggered by the this._withcommit () method (store. _research: false), you get an error.

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

Let’s take a look at the store structure after executing resetStoreVM. The store already has the getters property, and both getters and state are reactive.

At this point, the vuex core code initialization section has been analyzed. Source code also includes some plug-in registration and exposed API like mapState mapGetters mapActions mapMutation and other functions are not introduced here, interested can go to the source code to have a look, easier to understand. I won’t talk too much about it here.

conclusion

Vuex’s source code is well understood compared to vue’s. Before analyzing the source code, we suggest that we read the official documents carefully again, and write down the places that we do not understand, and read the source code with problems. Purposeful research can deepen memory. In the process of reading, you can first write a small example, introduce clone down the source code, step by step analysis of the implementation process.