1. What is Vuex?

Vuex is a set of state management patterns designed specifically for VUejS applications. It provides a set of concepts and usages for centrally managing data to solve the problem of large data sharing between components in medium and large projects. Its core concepts include state, mutations, and Actions. Based on these, its workflow is:

Vuex source code implementation process

Vuex finally exports an object that contains an install method and a Store class. Note how we use it

let store = new Vuex.Store({})
Copy the code

The install method is used by vue.use (), and internally vue.mixin blends a $store property into each component’s beforeCreate life cycle, which is that unique store.

Starting with new vuex.store (options), the main flow is as follows:

  1. Get the options passed in by the user and format it. The core is the register method
  2. Take the formatted data and install recursively from the root module, using installModules as the core

In addition to the above, Vuex also exports some auxiliary functions such as mapState, mapMutations and mapActions.

3. Why is mutation only sync?

In strict mode, mutation can only be synchronous. In non-strict mode, if you insist on using asynchrony, this is fine, but not recommended.

If we use an asynchronous method to change the state, imagine that we want to use logs to record the source of the state change. When two components change the same state, there should be two logs. Which component do these two logs correspond to?

Because it is asynchronous, the sequence of the two modification actions cannot be guaranteed, so we cannot judge the corresponding relationship between the log and the component. You may think it is the log of this component, but it is actually another component, which makes it impossible for us to debug and track state change information accurately.

4. How is state in VUEX responsive?

It is very simple to construct a Vue instance directly using new Vue() :

 this.vm = new Vue({
    data: {
        state: options.state// Reactive processing}});Copy the code

This makes state responsive, and you can see why Vuex is only used in Vue projects.

We also need another layer of proxies when accessing state:

get state() {// Get the state property on the instance to execute this method
    return this.vm.state
}
Copy the code

That’s not the end of it, of course. This is just for the outermost state, so if we write modules, what happens to the state of modules inside modules?

This is where we get to the heart of Vuex. We said that when a new Store instance is created, it will format the data that the user sends in. This is what the register method does. What is the end result of formatting?

Let’s take an example:

let store = new Vuex.Store({
    state: {// Single data source
        age: 10
    },
    getters: {
        myAge(state) {
            return state.age + 20; }},strict: true.// Only synchronization can be used in strict modemutations: { syncChange(state, payload) { state.age += payload; }},actions: {
        asyncChange({commit}, payload) {
            setTimeout((a)= > {
                commit('syncChange', payload);
            }, 1000); }},modules: {
        a: {
            namespaced: true.state: {
                age: 'a100'
            }, 
            mutations: {
                syncChange() {
                    console.log('a-syncChange'); }}},b: {
            namespaced: true.state: {
                age: 'b100'
            },
            mutations: {
                syncChange() {
                    console.log('b-syncChange'); }},modules: {
                c: {
                    namespaced: true.state: {
                        age: 'c100'
                    },
                    mutations: {
                        syncChange() {
                            console.log('c-syncChange'); }},}}}}});Copy the code

Let’s look at the final result

{
    _raw: rootModule,
    state: rootModule.state,
    _children: {
        a: {
            _raw: aModule,
            state: aModule.state,
            _children: {}
        },
        b: {
            _raw: aModule,
            state: aModule.state,
            _children: {
                c: {
                    _raw: cModule,
                    state: cModule.state,
                    _children: {}
                }
            }
        }
    }
} 
Copy the code

As you can see, this is still a tree structure. _raw is the module that the user wrote before formatting. State is separate because we use it when we install modules.

Highlight, register

 register(path, rootModule) {    
    let rawModule = {
        _raw: rootModule,// The module definition passed in by the user
        _children: {},
        state: rootModule.state
    }

    rootModule.rawModule = rawModule;// Two-way record

    if (!this.root) {
        this.root = rawModule;
    } else {
        // Find the parent of the current module
        let parentModule = path.slice(0.- 1).reduce((root, current) = > { / / register c [b, c]. Slice (0, 1) is equivalent to [b, c]. Slice (0, 1) is the result of the [b]
            return root._children[current]
        }, this.root);
        parentModule._children[path[path.length- 1]] = rawModule;
    }
    if (rootModule.modules) {
        forEach(rootModule.modules, (moduleName, module) = > {// Register a, [a] a module definition
            // Register b, [b] b module definition

            // Register the c, [b, c] c module definition
            this.register(path.concat(moduleName), module); }); }}Copy the code

Register takes two parameters. The first parameter is the path and the array type. For example, Vuex uses an array to determine module hierarchy. The second is the root module, which is the starting point for formatting, where the user passes in the data.

And then we see this sentence

 rootModule.rawModule = rawModule;// Two-way record
Copy the code

This is actually for dynamic registration, but we’ll talk about it later.

Here’s a classic look for dad:

For the root module, a root root is defined as the starting point, and the following sub-modules will go through forEach first, using path.concat(moduleName) to determine the module hierarchy, and then perform recursive registration. Here the forEach method is wrapped by ourselves:

let forEach = (obj, callback) = > {
    Object.keys(obj).forEach(key= >{ callback(key, obj[key]); })}Copy the code

Note the usage of slice, slice(0, -1) is to remove the current module, but not the current module

After finding the parent module of the current module, put the current module in the _children of the parent module, so that the registration of the parent module is complete.

The outermost state is already reactive, so how can the state inside modules be reactive?

This brings us to another of Vuex’s core installModules methods:

function installModule(store, rootState, path, rawModule) {// The rawModule used for installation is formatted data
    // The status of the submodule is installed
    // Determine whether to add the prefix according to the configuration passed by the current user
    let root = store.modules.root // Get the final formatting result
    let namespace = path.reduce((str, current) = > {
        root= root._children[current];// a
        str = str + (root._raw.namespaced ? current + '/' : ' ');
        return str;
    }, ' ');
    // console.log(path, namespace);
    if(path.length > 0) {// indicates a submodule
        // If it is c, find b first
        // [b,c,e] => [b, c] => c 
        let parentState = path.slice(0.- 1).reduce((rootState, current) = > {
            return rootState[current];
        }, rootState);

        // The responsivity of a vue cannot be responsified to attributes that do not exist
        Vue.set(parentState, path[path.length- 1], rawModule.state);
    }
    / / install getters
    let getters = rawModule._raw.getters;
    if (getters) {
        forEach(getters, (getterName, value) => {
            Object.defineProperty(store.getters, namespace + getterName, {
                get: (a)= > {
                    // return value(rawModule.state); // rawModule is the current module
                    returnvalue(getState(store, path)); }}); }); }/ / install mutation
    let mutations = rawModule._raw.mutations;
    if (mutations) {
        forEach(mutations, (mutationName, value) => {
            let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
            arr.push((payload) = > {
                // value(rawModule.state, payload); // where mutation is actually performed
                value(getState(store, path), payload);
                store.subs.forEach(fn= > fn({type: namespace + mutationName, payload: payload}, store.state)); // This is a slice
            });
        });
    }
    The action / / installation
    let actions = rawModule._raw.actions;
    if (actions) {
        forEach(actions, (actionName, value) => {
            let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
            arr.push((payload) = > {
                value(store, payload);
            });
        });
    }
    // Process submodules
    forEach(rawModule._children, (moduleName, rawModule) => {
        installModule(store, rootState, path.concat(moduleName), rawModule)
    });
}
Copy the code

The installModules method takes four arguments:

  1. Store: new vuex.store ()
  2. RootState: indicates the outermost state
  3. Path: module hierarchy
  4. RawModule: a formatted module

Let’s just focus on this code for now:

 if(path length > 0) {/ / that / / if the is c is the child module, first find b / / / b, c, e = > (b, c) = > clet parentState = path.slice(0, -1).reduce((rootState, current) => {
        returnrootState[current]; }, rootState); Vue. Set (parentState, path[path.length-1], rawModule.state); }Copy the code

You see, I’m still looking for the dad. I was looking for the parent module, but this time I’m looking for the state of the parent module. The vue.set () method is then used to make the modules under Modules responsive. And then we’re still recursing

 // Process submodules
forEach(rawModule._children, (moduleName, rawModule) => {
    installModule(store, rootState, path.concat(moduleName), rawModule)
});
Copy the code

5. Implementation principle of Getters

With that in mind, let’s go straight to installModules:

 / / install getters
let getters = rawModule._raw.getters;
if (getters) {
    forEach(getters, (getterName, value) => {
        Object.defineProperty(store.getters, namespace + getterName, {
            get: (a)= > {
                // return value(rawModule.state); // rawModule is the current module
                returnvalue(getState(store, path)); }}); }); }Copy the code

There are two new things involved:

  1. namespace
  2. getState

Let’s start with namespace and look at the following code

 // Determine whether to add the prefix according to the configuration passed by the current user
let root = store.modules.root // Get the final formatting result
let namespace = path.reduce((str, current) = > {
    root= root._children[current];// a
    str = str + (root._raw.namespaced ? current + '/' : ' ');
    return str;
}, ' ');
Copy the code

Again reduce, the original method XXX becomes a/b/ XXX with the addition of the namespace.

GetState method:

// Get the latest status of each module recursively
function getState(store, path) {   
    let local = path.reduce((newState, current) = > {
        return newState[current];
    }, store.state);
    return local;
}
Copy the code

Or reduce, getState is mainly used in combination with Vuex to realize data persistence, which will be introduced below and skipped here.

Ok, now that we know about these two things, we can look at getters and see that eventually all getters are defined in the store. Getters Object using Object.defineProperty. Note that this is essentially smoothing out the original tree structure, which is why it is important to ensure that namespaces are not duplicated when we do not apply them.

Mutations implement the principle

/ / install mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
    forEach(mutations, (mutationName, value) => {
        let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
        arr.push((payload) = > {
            value(getState(store, path), payload);
        });
    });
}
Copy the code

Mutations cause mutations. Once you know the principle of getters, you also know the principle of mutations, which means subscribe, and commit means publish.

7. Implementation principle of Actions

 The action / / installation
let actions = rawModule._raw.actions;
if (actions) {
    forEach(actions, (actionName, value) => {
        let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
        arr.push((payload) = > {
            value(store, payload);
        });
    });
}
Copy the code

Again, subscribe and publish.

8. How to implement dynamic registration module?

Dynamic registration is a class method registerModule provided by Vuex

// Dynamic registration
registerModule(moduleName, module) {
    if (!Array.isArray(moduleName)) {
        moduleName = [moduleName];
    }
    this.modules.register(moduleName, module); // This is just formatting
    installModule(this.this.state, moduleName, module.rawModule);// Start from the current module
}
Copy the code

It takes two parameters. The first parameter is the name of the module to register. The path in the register method should be an array type. The second is the corresponding option. Internally, the process of formatting and then installing was done using the bidirectional binding mentioned above

rootModule.rawModule = rawModule;
Copy the code

The corresponding

rawModule._raw = rootModule;
Copy the code

RootModule is a module before formatting, and rawModule is a module after formatting. rootModule

Note that the installation starts with the current module, not the root module.

Usage:

Register a single module

store.registerModule('d', {
    state: {
        age: 'd100'}});Copy the code

Register a hierarchical module

store.registerModule(['b'.'c'.'e'] and {state: {
        age: 'e100'}});Copy the code

9. Namespace

In multiple modules, why can’t the same name be used if namespaces are not used?

The implementation principle of getters has been mentioned above, because when installing getters, mutations, actions, it is either paved in an object or in an array. If the names are the same and namespaces are not used, there will be conflicts.

10. Vuex plug-in

In addition to state, getters, Mutations, actions, and so on, there is also a plugins option in the user options, which exposes a hook for each mutation. Combined with the SUBSCRIBE method provided by Vuex, each mutation information can be monitored.

Plug-ins are functions that are executed once when new vuex.store () is executed

this.subs = [];
let plugins = options.plugins;
plugins.forEach(plugin => plugin(this));
Copy the code

Internally, it’s still a publish and subscribe application, and here we implement two common plug-ins

  1. logger
  2. State persistence

Before we do that let’s take a look at the Subscribe method provided by Vuex, which is a class method

subscribe(fn) {
    this.subs.push(fn);
}
Copy the code

The location of the distribution must be associated with mutation, so we can change the installation of mutation

/ / install mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
    forEach(mutations, (mutationName, value) => {
        let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
        arr.push((payload) = > {
            value(getState(store, path), payload);
            store.subs.forEach(fn= > fn({type: namespace + mutationName, payload: payload}, store.state)); // This is a slice
        });
    });
}
Copy the code

After mutation, each method in the subs was executed and published. Here we also see another programming highlight: slicing,

arr.push((payload) => {
    value(getState(store, path), payload);
    store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); });Copy the code

Here if we don’t think about subscribe, we can write it as subscribe

arr.push(value);
Copy the code

Since value is a mutation and a method, it can be stored directly, but then there is no way to do other things. Using slicing is convenient for us to expand, which is the charm of slicing programming.

Let’s look at the implementation of the Logger plug-in

function logger(store) {
    let prevState = JSON.stringify(store.state);// The default state requires a deep copy with json.stringify, otherwise preState is a reference type, so when store.state changes, preState changes immediately, and the last state cannot be printed
    // let prevState = store.state; // Default state
    / / subscribe
    store.subscribe((mutation, newState) = > {// This method is executed each time mutation is called
        console.log(prevState);
        console.log(mutation);
        console.log(JSON.stringify(newState));
        prevState = JSON.stringify(newState);// Save the latest status
    });
}
Copy the code

Data persistence

function persists(store) {
    let local = localStorage.getItem('VUEX:state');
    if (local) {
        store.replaceState(JSON.parse(local));// all states will be replaced with local
    }
    store.subscribe((mutation, state) = > {
        // Many changes are recorded only once, need to do a shake
        debounce((a)= > {
            localStorage.setItem('VUEX:state'.JSON.stringify(state));
        }, 500) (); }); }Copy the code

The principle is to use the browser’s own API localStorage, there is a new method replaceState, which is also a class method

replaceState(newState) {
    this.vm.state = newState;
}
Copy the code

This method is used to update the state. It should be noted here that we only change the state, while getters, mutations, and Actions still use the old state, which is decided at the time of installation, so we also need to make them get the latest state every time they execute. So remember the getState method above?

A minor optimization was made with throttling to cope with continuous state changes, but throttling won’t be discussed here.

Finally, let’s talk about the use of plug-ins:

plugins: [
    persists,
    logger 
],
Copy the code

Note the Logger method. The first time the state changes, prev state contains only non-dynamically registered modules, and next State contains all modules. This is because the store passed in when the Logger method is first executed does not yet contain dynamically registered modules.

11. Implementation principle of auxiliary functions

Vuex provides several auxiliary methods such as mapState, mapGetters, mapMutations and mapActions to facilitate our writing. We divide these into two categories:

  1. MapState and mapGetters are two things that we use in computed
computed: { ... mapState(['age']),//. mapGetters(['myAge'])
    /* age() { return this.$store.state.age; } */
},
Copy the code
  1. MapMutations and mapActions are used in methods
 methods: {
    / /... mapMutations(['syncChange']),. mapMutations({aaa: 'syncChange'}),// Use an alias. mapActions({bbb: 'asyncChange'})}Copy the code

mapState

export function mapState (stateArr) {// {age: fn}
    let obj = {};
    stateArr.forEach(stateName => {
        obj[stateName] = function() {
            return this.$store.state[stateName]; }});return obj;
}
Copy the code

mapGetters

export function mapGetters(gettersArr) {
    let obj = {};
    gettersArr.forEach(getterName= > {
        obj[getterName] = function() {
            return this.$store.getters[getterName]; }});return obj;
}
Copy the code

mapMutations

export function mapMutations(obj) {
    let res = {};
    Object.entries(obj).forEach(([key, value]) => {
        res[key] = function(... args) { this.$store.commit(value, ...args);
        }
    });
    return res;
}
Copy the code

mapActions

export function mapActions(obj) {
    let res = {};
    Object.entries(obj).forEach(([key, value]) => {
        res[key] = function(... args) { this.$store.dispatch(value, ...args);
        }
    });
    return res;
}
Copy the code

We have implemented the parameters passed in to the array and object types respectively, and the source code is compatible with both using the normalizeMap method. An object is returned, so we need to deconstruct it when we use it.

The complete code

let Vue;

let forEach = (obj, callback) => { Object.keys(obj).forEach(key => { callback(key, obj[key]); })} class ModuleCollection {constructor(options) {constructor(options) {this.register([], options); } register(path, rootModule) {letRawModule = {_RAW: rootModule,// Module definition passed by user _children: {}, state: rootModule.state} rootModule.rawModule = rawModule; // Two-way recordif(! this.root) { this.root = rawModule; }else{// The classical method finds the parent of the current moduleletParentModule = path.slice(0, -1).reduce((root, current) => {// Register c when [b, C]. Slice (0, -1) => {// Register C when [b, C]. Slice (0, -1) => {// Register C when [b, C]. 1) The result is [b]return root._children[current]
            }, this.root);
            parentModule._children[path[path.length-1]] = rawModule;
        }
        if (rootModule.modules) {
            forEach(rootModule.modules, (moduleName, module) => {// Register a, [a] // register b, [b] // register c, [b, This. Register (path.concat(moduleName), module); }); }}} // Get the latest status of each module recursivelyfunction getState(store, path) {   
    let local = path.reduce((newState, current) => {
        return newState[current];
    }, store.state);
    return local;
}
functionInstallModule (Store, rootState, Path, rawModule) {// The rawModule used for installation is formatted data. // The status of submodules to be installed. // Determine whether to add the prefix based on the configurations passed by the current userletRoot = store.modules. Root // Gets the final formatting resultletnamespace = path.reduce((str, current) => { root= root._children[current]; // a str = str + (root._raw.namespaced ? current +'/' : ' ');
        return str;
    }, ' ');
    // console.log(path, namespace);
    if(path length > 0) {/ / that / / if the is c is the child module, first find b / / / b, c, e = > (b, c) = > clet parentState = path.slice(0, -1).reduce((rootState, current) => {
            returnrootState[current]; }, rootState); Vue. Set (parentState, path[path.length-1], rawModule.state); } // Install getterslet getters = rawModule._raw.getters;
    if (getters) {
        forEach(getters, (getterName, value) => {
            Object.defineProperty(store.getters, namespace + getterName, {
                get: () => {
                    // returnvalue(rawModule.state); // rawModule is the current modulereturnvalue(getState(store, path)); }}); }); } // install mutationlet mutations = rawModule._raw.mutations;
    if (mutations) {
        forEach(mutations, (mutationName, value) => {
            letarr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []); arr.push((payload) => { // value(rawModule.state, payload); Value (getState(store, path), payload); store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); }); }); } // Install actionlet actions = rawModule._raw.actions;
    if (actions) {
        forEach(actions, (actionName, value) => {
            letarr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []); arr.push((payload) => { value(store, payload); }); }); } // Process submodulesforEach(rawModule._children, (moduleName, rawModule) => { installModule(store, rootState, path.concat(moduleName), rawModule) }); } class Store { constructor(options) { // console.log(options); // this. State = options.state; This.vm = new Vue({data: {state: options.state// reactive processing}}); this.getters = {}; 2. mutations = {}; this.actions = {}; Modules = new ModuleCollection(options); InstallModule (this, this.state, [], this.modules. Root); this.subs = [];letplugins = options.plugins; plugins.forEach(plugin => plugin(this)); } subscribe(fn) { this.subs.push(fn); } replaceState(newState) {this.vm.state = newState; Getters mutations Actions still uses the old status, which was decided at install time, so we also need them to get the latest status every time they perform} getstate() {// Get the state property on the instance to execute this methodreturnThis.vm. state} commit = (mutationName, payload) => { Mutations [mutationName](payload); // Mutations [mutationName](mutations); this.mutations[mutationName].forEach(mutation => mutation(payload)); } dispatch = (actionName, payload) => { // this.actions[actionName](payload); this.actions[actionName].forEach(action => action(payload)); } // Register registerModule(moduleName, module) {if(! Array.isArray(moduleName)) { moduleName = [moduleName]; } this.modules.register(moduleName, module); InstallModule (this, this.state, moduleName, module.rawModule); }} const install = (_Vue) => {Vue = _Vue; // It is not right to put it on the prototype because all Vue instances are added by default$storeProperty // We want to start with only the current root instance, up to all of its children$storeAttribute Vue. Mixin ({beforeCreate() {
            // console.log('This is the one in mixin', this.$options.name); // Put the store property of the root instance in each componentif (this.$options.store) {
                this.$store = this.$options.store;
            } else {
                this.$store = this.$parent && this.$parent.$store; }}}); // Remove common logic}export function mapState (stateArr) {// {age: fn}
    let obj = {};
    stateArr.forEach(stateName => {
        obj[stateName] = function() {
            return this.$store.state[stateName]; }});return obj;
}

export function mapGetters(gettersArr) {
    let obj = {};
    gettersArr.forEach(getterName => {
        obj[getterName] = function() {
            return this.$store.getters[getterName]; }});return obj;
}

export function mapMutations(obj) {
    let res = {};
    Object.entries(obj).forEach(([key, value]) => {
        res[key] = function(... args) { this.$store.commit(value, ...args);
        }
    });
    return res;
}
export function mapActions(obj) {
    let res = {};
    Object.entries(obj).forEach(([key, value]) => {
        res[key] = function(... args) { this.$store.dispatch(value, ...args);
        }
    });
    return res;
}
export default {
    install,
    Store
}
Copy the code