This article mainly explains how Vuex is implemented at the bottom level, so that we can really understand and understand the internal implementation mechanism of Vuex. How to read it? Knock out the bottom vuex hand yourself. First of all,State in Vuex is implemented using responsiveness in VueAnd second,Use publish subscriptions to implement mutations and actions, and Object.defineProperty to implement gettersFinally, againDelve into modules implementation mechanisms. The article may be a little long, as long as you carefully read, will certainly gain.

If you think it is helpful for you to learn VUEX, I hope you can order a small onepraiseOh!!!!!! Ok, no more words, officially enter the article theme!!

Vuex concept

Vuex is a state management mode 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. In fact, Vuex is a container for storing state. Each component can obtain the state of the shared component through Vuex, which is actually the communication process between components. The operation process of Vuex is shown below:

Core principles of Vuex

  • Vuex Vue-RouterIt’s all the same, it’s all a plugin for Vue,Essence is an object.
  • VuexObject has two properties, one isThe install method, one isStore the class.
  • The install methodIs the function ofMount the Store instance to each component.
  • StoreIn this class, containsCommit, dispatchAnd so on, through the internalNew Vue to use Vue's response to achieve Vuex's internal response.

Ok, now that we know how vuex works internally, are we going to implement Vuex

The install method

The install method basically makes each component mount an instance of $Store. This.$store allows each component to obtain the state and methods in Vuex.

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store/store.js'
Vue.config.productionTip = false

new Vue({
  store,
  render: h= > h(App),
}).$mount('#app')

Copy the code
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
const applyMinx = (Vue) = > {
    // Each component needs to be mixed with the mount $store property
    Vue.mixin({
        beforeCreate:vuexInit
    })
}
// The child component is created after the parent
function vuexInit () {
    const options  = this.$options
    // Bind $store to the root component
    if(options.store) {
        this.$store = options.store 
    } else if (options.parent && options.parent.$store) {
        // There is a parent component and the parent component has the $store property
        // The non-root component fetches $store from the parent component to bind to its own component instance
        this.$store =  options.parent.$store
    }
}
Copy the code

VuexInit ($options) {vuexInit ($options) {vuexInit ($options) {vuexInit ($options) {vuexInit ($options); $options.store ($options.store); $options.store ($options.store); $options.store ($options.store); If there is no store attribute, it indicates that the child component has a parent component and the parent component has $store attribute. Then add $store attribute to the child component.

Store the class

import Vue from 'vue'
import Vuex from '.. /vuex/index.js'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            return state.age
        }
    },
    modules: {},
    actions: {
        changeAge({commit},payload) {
            setTimeout(() = >{
                commit('getAge',payload)
            },1000)}}})Copy the code
import applyMinx from './mixin'
let Vue
class Store {
    constructor(options) {
        $store. State can render data on the component, but the data is not reactive
        this.state = options.state
      }
}
Copy the code
// App.vue
<template>
  <div id="app">
      state: {{$store.state.age}}
      <button @click="handle">Click on the + 10</button>
  </div>
</template>

<script>

export default {
  name: 'App'.methods: {
    handle() {
      this.$store.state.age += 10
      console.log(this.$store.state.age); }}}</script>
Copy the code

$store. State (mutation) : $store. State (mutation) : $store.

Test results:

And then you see that you can render it to the page normally, and you can render it to the page, but,Now this data is not reactiveThat is, when I change the value of state in store with a button click, at this pointWill not trigger page re-rendering, data update, it is also aDifferences between Vuex and global variables(Interview test points).Vuex is responsiveAnd theGlobal variables are not reactive. Speaking of the difference between Vuex and global variables, let’s briefly explain the difference between Vuex and global variables:

Differences between Vuex and global variables

  • The state store for Vuex isresponsive. When the Vue component reads the state from the store, ifstoreThe state changes in, then the correspondingComponents are efficiently updated accordingly. At the same timeYou cannot modify store directlyIn, onlyChange the state by showing commit mutation.
  • Vuex byUnified approach to modifying dataGlobal variables doAny modification.
  • Too many global variables can causeNamespace pollution, Vuex won’t, meanwhileVuex also addresses the issue of cross-component communication.

State

Through analysis, we know that the above method of directly assigning options.state to this.state is not available, so we need to set the state in store as responsive. Here vuex cleverly makes use of the responsive principle of Vue. Responsiveness is achieved by storing the state in store into the VUE instance as an attribute of data. The code is as follows:

import applyMinx from './mixin'
let Vue
class Store {
    constructor(options) {
        $store. State can render data on the component, but the data is not reactive
        let state = options.state
        this._vm = new Vue({
            // Define data in vue. If the attribute name is $XXX, it will not be proxied to the vue instance and stored in _data
            // If you don't want to get it from this._vm.state, use $to get it from this._vm_data
            data() {
                return {// $$internal status
                    $$state: state
                }
            },
        })  
    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // The data is responsive
        return this._vm._data.$$state
    }
    
}
Copy the code

$Store. State = $Store. State = $Store. $Store. State = $Store. Vue has internally hijacked the data in data to achieve responsiveness. When accessing vUE instance objects, the instance object data will also be responsiveness. Test results:

And what we found by testing is that the data at this point is zeroresponsiveAnd whenWhen the store state changes, the page will also render the new value.

Getters

With state implemented, let’s move on to implementing Vuex’s getters method. When using vuex getters, have you noticed that the declared getters are a method, but when we use vuex getters, we can directly write the attribute value to get the data?

// Write the method.getters: {
        getAge(state) {
            return state.age
        }
    },
...
Copy the code
// Get attributes
<template>
  <div id="app">
      state: {{$store.state.age}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">Click on the + 10</button>
  </div>
</template>
Copy the code

DefineProperty (object.defineProperty); getGetMethod (object.defineProperty);

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        $store. State can render data on the component, but the data is not reactive
        let state = options.state
        // Use the same method as in store
        this._getters = {}
        // the method written by getters gets attributes
        GetAge is the key function. GetAge is the property of the value getmethod
        Object.keys(options.getters).forEach(key= > {
            Object.defineProperty(this.getters,key,{
                get:() = > {
                    return options.getters[key](this.state)
                }
            })
        })
        this._vm = new Vue({
            // Define data in vue. If the attribute name is $XXX, it will not be proxied to the vue instance and stored in _data
            // If you don't want to get it from this._vm.state, use $to get it from this._vm_data
            data() {
                return {// $$internal status
                    $$state: state
                }
            },
        })
    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
}
Copy the code

In the code above. We declare a _getters Object on the Store class to Store the getters method in the Store, iterate over the key of the getters in the Store, place it on _getters, and then use Object.defineProperty to delegate. When accessing the getter property, the get method is triggered and the options.getters function is automatically executed. It will return it. Test results:

If you test it, you’ll see that getters can be displayed on the page and implementedresponsiveThis is because inGet the getters propertyTriggered whengetAnd theGet executes a function that contains this.state, soState implements responsivenessIndirect getters are also implementedresponsive.

But hereGetters has no caching effectIf the value of the dependency has not changed, the method in getters will be executed. The tests are as follows:

.state: {
        age: 10.a: 1
    },
getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
...
Copy the code
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">Click on a + 10</button>
  </div>
</template>

<script>

export default {
  name: 'App'.methods: {
    handle() {
    // Change only the value of a
      this.$store.state.a += 10}}}</script>

Copy the code

The age value does not change, but the getAge method of getters is always executed becauseThe data for state has changed, will update the page and in the pageAlways get the getAge value of gettersAnd at this pointAlways trigger the GET method, re-executemethodThis is not what we want to see. So, we need toCache getters. So how do you cache? To review whether some of vUE’s computed data has caching effects,Vuex makes clever use of vUE's computed caching featureTo solveVuex getters cacheQuestion, so this is what we often say,vuextheGetters propertiesIs equivalent tovueIn theComputed propertiesThe state attribute of vuex is equivalent to data in VUE, in factVuex internally instantiates a VUEAnd thenGetters uses VUE's computed to implement caching, andState is implemented by using the data response in VUE. (You may be asked why we often say that vuex state and getters are data and computed in VUE, and what do you think of that?)

Implement getters cache

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        // console.log(options);
        $store. State can render data on the component, but the data is not reactive
        let state = options.state
        // object.create differs from {} :
        // object.create (null) The prototype chain points to null {} The prototype chain points to Object
        // Store the user-defined getters
        this.getters = Object.create(null)
        
        // The cache computes attributes using vUE's computed attributes and puts its own attributes on the instance
        const computed = {}
        // the method written by getters gets attributes
        GetAge is the key function. GetAge is the property of the value getmethod
        Object.keys(options.getters).forEach(key= > {
            // Cache uses vUE to compute attributes for lazy loading
            computed[key] = () = > {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:() = > {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // Define data in vue. If the attribute name is $XXX, it will not be proxied to the vue instance and stored in _data
            // If you don't want to get it from this._vm.state, use $to get it from this._vm_data
            data() {
                return {// $$internal status
                    $$state: state
                }
            },
            computed  This._vm. A is the a that gets the calculated property})}// Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code

A computed property in vUE puts its own attributes on the VUE instance, which can be passed directly through the instance. Property to get the value for computed. Test results:

Perfect, we implemented getters cache effect.

Mutations

After completing the function of getters, we will continue to improve the methods of mutations. Through the publishing and subscription model in Vuex, the mutations and actions defined by users will be stored, and the subscription solutions will be found when the COMMIT is triggered. Look for the subscription Actions method when dispatch is triggered.

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        $store. State can render data on the component, but the data is not reactive
        let state = options.state
        // object.create differs from {} :
        // object.create (null) The prototype chain points to null {} The prototype chain points to Object
        // Store the user-defined getters
        this.getters = Object.create(null)
        
        // The cache computes attributes using vUE's computed attributes and puts its own attributes on the instance
        const computed = {}
        // the method written by getters gets attributes
        GetAge is the key function. GetAge is the property of the value getmethod
        Object.keys(options.getters).forEach(key= > {
            // Cache uses vUE to compute attributes for lazy loading
            computed[key] = () = > {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:() = > {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // Define data in vue. If the attribute name is $XXX, it will not be proxied to the vue instance and stored in _data
            // If you don't want to get it from this._vm.state, use $to get it from this._vm_data
            data() {
                return {// $$internal status
                    $$state: state
                }
            },
            computed  This._vm. A is the a that gets the calculated property
        })
        Publish and subscribe stores user-defined mutations and actions looking for the subscribe solutions method when commit is called
        // Call Dispatch and find the subscribe Actions method

        / / method of mutations
        this._mutations = Object.create(null)
        Object.keys(options.mutations).forEach(key= > {
            // Publish subscribe mode
            this._mutations[key] = (playload) = > {
                // The first argument points to state
                // call ensures that this always points to the current store instance
                options.mutations[key].call(this.this.state,playload)
            }
        })
    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    commit = (type,playload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type](playload)
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code
// APP.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('changeAge',5)">Mustation method</button>
  </div>
</template>
Copy the code
.mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
...
Copy the code

Actions

The Actions method is the same as the mutations method, which stores the subscription by publishing it, and then finds the actions method of the subscription when the dispatch is triggered.

import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
    constructor(options) {
        $store. State can render data on the component, but the data is not reactive
        let state = options.state
        // object.create differs from {} :
        // object.create (null) The prototype chain points to null {} The prototype chain points to Object
        // Store the user-defined getters
        this.getters = Object.create(null)
        
        // The cache computes attributes using vUE's computed attributes and puts its own attributes on the instance
        const computed = {}
        // the method written by getters gets attributes
        GetAge is the key function. GetAge is the property of the value getmethod
        Object.keys(options.getters).forEach(key= > {
            // Cache uses vUE to compute attributes for lazy loading
            computed[key] = () = > {
                return options.getters[key](this.state)
            }
            Object.defineProperty(this.getters,key,{
                get:() = > {
                    return this._vm[key]
                }
            })
        })
        this._vm = new Vue({
            // Define data in vue. If the attribute name is $XXX, it will not be proxied to the vue instance and stored in _data
            // If you don't want to get it from this._vm.state, use $to get it from this._vm_data
            data() {
                return {// $$internal status
                    $$state: state
                }
            },
            computed  This._vm. A is the a that gets the calculated property
        })
        Publish and subscribe stores user-defined mutations and actions looking for the subscribe solutions method when commit is called
        // Call Dispatch and find the subscribe Actions method

        / / method of mutations
        this._mutations = Object.create(null)
        Object.keys(options.mutations).forEach(key= > {
            // Publish subscribe mode
            this._mutations[key] = (playload) = > {
                // The first argument points to state
                // call ensures that this always points to the current store instance
                options.mutations[key].call(this.this.state,playload)
            }
        })
        
        / / actions
        this._actions = Object.create(null)
        Object.keys(options.actions).forEach(key= > {
            this._actions[key] = (playload) = > {
                // The first argument will be this because here we always deconstruct {commit}
                options.actions[key].call(this.this, playload)
            }
        })
    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    commit = (type,playload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type](playload)
    }
    dispatch = (type,playload) = > {
        this._actions[type](playload)
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code
//App.vue
<template>
  <div id="app">
      state.age: {{$store.state.age}}
      satte.a:{{$store.state.a}}
      getters: {{$store.getters.getAge}}
      <button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code
.actions: {
        changeAge({commit},playload) {
            setTimeout(() = >{
                commit('getAge',playload)
            },1000)}}...Copy the code

We can see that the actions change is executed after 1s, which is consistent with the timer we wrote in store, and it’s doneFunction of the actionsThe implementation of the.

At this point, vuex’s main implementation mechanism is complete, but modules is not implemented yet, but vuex’s main logic is already running. Now that WE’re 70% of the way through vuex, let’s explore how modules are implemented

Modules Basic usage

Before we write modules by hand, let’s take a look at the basic usage of modules in VUex.

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10.a: 1
    },
    mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('getAge',playload)
            },1000)}},modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            state: {
                e:100
            },
            mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}}}}})Copy the code

Mutations method add two submodules, B template and D module, to modules, and the mutations method inside is the same name as the mutations of the root module. See the test result:

And what you find is,By default, when mutations have the same name, there is no scope between modules, where the function is combinedDon't coverInstead, the function is stored in an array. whenTo trigger a commit“Will be in turnExecute the functions in the array.

Add a submodule E to module D, and you will find that the name of module D is the same as the name of module E under state. Test results:

// APP.vue
<template>
  <div id="app">State.age: {{$store.state.age}} getters: {{$store.getters. GetAge}} b module c {{$store.state.b.c}} D module E {{$store.state.d.e}}<button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code
import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,},mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('getAge',playload)
            },1000)}},modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            state: {
                e:300
            },
            mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}},modules: {
                e: {
                    state: {
                        g:500
                    },
                    mutations: {}}}}}})Copy the code

When the module name and state name are the same, $store.state.d.e does not obtain the state e of module d, but obtains the state of sub-modules under module d. So,It is recommended that the module name be different from the status name.

Add a getters property to the D module, and you’ll notice

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,},mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('getAge',playload)
            },1000)}},modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            state: {
                e:300
            },
            // Add new code
            getters: {
                getD(state) {
                    return state.e += 50}},mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}},modules: {}}}})Copy the code
// APP.vue
<template>
  <div id="app">state.age: {{$store.state.age}} getters: {{$store.state.b.c}} d {{$store.state.d.e}} getD: {{$store.getters.d.getage}} d {{$store.state.d.getage} d {{$store.state.d.getage}<button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code

If you go straight through the module. Property to get the value of getters is not available. As we said earlier,Computed attributes put their own attributes on instancesAnd,computedgettersIs the same, which means,The properties of Getters also mount their own properties directly to the instance, so,By defaultWe can go straight throughGetters. Property acquisitionGetters without writing the module name.

In the way I wrote it above, we didn’t even add namespaced namespace attributes, so let’s add Namespaced and see what happens to our code.

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,},mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('changeAge',playload)
            },1000)}},modules: {
        b: {
            // Add new code
            namespaced: true.state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50}},mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}},modules: {}}}})Copy the code

Use rules: module name/state or method or getters

// App.vue
<template>
  <div id="app">state.age: {{$store.state.age}} getters: {{$store.state.b.c}} d {{$store.state.d.e}} getD: {{$store.getters. GetD}}<button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('b/changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code

Instead of implementing all results, the mutations method will only be implemented with the Namespaced: True module, and you’ll notice that when the mutations are added to the moduleNamespaced are trueAnd then, at this point, it’s going to beThe module's getters, state, mutations, and actions are encapsulated in the middle scope.

If I add a namespace to a submodule of a submodule, for example, if I add a namespace to a submodule of a submodule of d, do I get values from d/f/changeAge? Let’s test it to see how it works:

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,},mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('changeAge',playload)
            },1000)}},modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50}},mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}},modules: {
                // Add new code
                f: {
                    namespaced: true.state: {
                        g:700
                    },
                    mutations: {
                        changeAge() {
                            console.log('G in f under d is updated')}}}}}}})Copy the code
//App.vue
<template>
  <div id="app">state.age: {{$store.state.age}} getters: {{$store.state.b.c}} d {{$store.state.d.e}} getD: {{$store.getters. GetD}}<button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('d/f/changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code

The test found the message could not find the method and passedD /f/changeAge cannot be obtainedIf not, the current module is used as a scope. If not, it is nested, so you can pass it directlyf/changeAgeMethod to obtainmutationsThe method of. Test results after modification:

At this time, remove the namepaced of module F and add namespace for module D.

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 10,},mutations: {
        changeAge(state,num) {
            state.age += num
        }
    },
    getters: {
        getAge(state) {
            console.log('getAge executes. ');
            return state.age
        }
    },
    actions: {
        getAge({commit},playload) {
            setTimeout(() = >{
                commit('changeAge',playload)
            },1000)}},modules: {
        b: {
            state: {
                c:100
            },
            mutations: {
                changeAge() {
                    console.log('C in B is updated')}}},d: {
            namespaced: true.state: {
                e:300
            },
            getters: {
                getD(state) {
                    return state.e += 50}},mutations: {
                changeAge() {
                    console.log(The e in'd 'is updated.)}},modules: {
                f: {
                    state: {
                        g:700
                    },
                    mutations: {
                        changeAge() {
                            console.log('G in f under d is updated')}}}}}}})Copy the code
//App.vue
<template>
  <div id="app">state.age: {{$store.state.age}} getters: {{$store.state.b.c}} d {{$store.state.d.e}} getD: {{$store.getters. GetD}}<button @click="handle">Click on a + 10</button>
      <button @click="$store.commit('d/changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>
Copy the code

At this point, throughD/changeAge trigger, you will find that e, which executes D, is updated, and g, which executes F, which executes D, is also updatedModule has namespacedWhen, this time will beThe current module encapsulates a scope, submodules under d module if notNamespaced attribute, the remaining submodules all belong to the current declarationNamespaced module management.

The basic usage of modules is summarized as follows:

  • Modules, by default, have no scope problems.
  • The status name should not be the same as the module name.
  • Properties are computed by default, and can be evaluated directly via getters.
  • Adding namespaced: True encapsulates the module's properties in this scope.
  • By default, it checks whether the current module has a Namespaced attribute, and then checks whether the parent has a namespaced attribute. If it does, it counts as a scope, and if it doesn't, the current module is a scope.

Okay, now that we know the basics of modules, let’s formally write modules by hand

Modules

In the above state, getters, mutations, actions, we only made the root module, and did not take modules into account. Therefore, we cannot nest recursion in the code we wrote before. We will rewrite a Store class, but the previous work is not in vain. With the previous knowledge, we know the internal mechanism, we can easily write a complete Store class, and we can easily understand the internal core of Vuex.

ModuleCollection class

The main purpose of this class is to collect modules, and then format parameter modules into a nested tree of modules for subsequent operations.

Mutations, actions, and state _raw: XXX, // Submodule _children: {a: {_raw: xxx, _children: {} state: xxx.state }, b: { _raw: xxx, _children: {}, state: xxx.state } }, state: xxx.state }Copy the code

Now that you know how to format the arguments passed in by the user into the above tree structure, it’s time to implement the moduleCollection class.

import forEach from "./utils";

export default class ModlueCollection {
    constructor(options) {
        // register module [] indicates path recursive register module
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = {
            _raw: rootModule,
            _children: {},
            state: rootModule.state
        }
        // If the array length is, it is the root module
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // Indicates that path has a length such as [a,b]. Store this module in the children property of the root module
            let parent = path.slice(0, -1).reduce((pre, next) = > {
                // Find the parent node step by step
                return pre._children[next]
            },this.root)
            parent._children[path[path.length-1]]  = newModlue
        }
        // Register submodules
        if(rootModule.modules) {
            forEach(rootModule.modules,(moduleValue, moduleName) = >{
                // Recursively form submodules to pass in module names and module values
                this.register([...path,moduleName],moduleValue)
            })
        }

    }
}
Copy the code

Through testing, we organized the incoming parameters into a nested tree structure. The above code is still a bit redundant, so let’s extract the code and create a class for each module.

// modules.js
export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // Find the submodule of the module
    getChild(key) {
        return this._children[key]
    }
    // Append submodules to modules
    addChild(key, module) {
        this._children[key] = module}}Copy the code
// module-collection.js
import forEach from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // register module [] indicates path recursive register module
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        / / {
        // _raw: rootModule,
        // _children: {}
        // state: rootModule.state
        // }
        // If the array length is, it is the root module
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // Indicates that path has a length such as [a,b]. Store this module in the children property of the root module
            let parent = path.slice(0, -1).reduce((pre, next) = > {
                // Find the parent node step by step
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]] = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // Register submodules
        if(rootModule.modules) {
            forEach(rootModule.modules,(moduleValue, moduleName) = >{
                // Recursively form submodules to pass in module names and module values
                this.register([...path,moduleName],moduleValue)
            })
        }

    }
}
Copy the code

After we remove the code, the effect is still the same, but the idea is clearer. For example, getChild is to find submodules, addChild is to append modules to submodules, and now we add each Module’s method to each Module’s Module. After transforming the parameters passed in by the user into a recursive tree structure, we are back to the Store class we wrote earlier. We need to register the state, mutations, actions and getters methods of all modules on the Store.

Customize the forEachValue method

/ * * * *@param {*} Obj Passed object *@param {*} The callback function deconstructs the value and key */, respectively
const forEachValue = (obj = {}, callback) = > {
    Object.keys(obj).forEach(key= > {
        // The first argument is the value, and the second argument is the key
        callback(obj[key],key)
    })
}
export default forEachValue
Copy the code

The purpose of wrapping this method is to make it easy to get the key-value pairs of the object.

installModlue

The installModlue method installs the states and methods in the created tree structure to the Store instance, and then the corresponding data can be obtained through $Store.

// module.js
import forEachValue from "./utils"

export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // Find the submodule of the module
    getChild(key) {
        return this._children[key]
    }
    // Append submodules to modules
    addChild(key, module) {
        this._children[key] = module
    }
    // open mutations for the current module
    forEachMutations(fn) {
        if(this._raw.mutations) {
            forEachValue(this._raw.mutations,fn)
        }
    }
    // Iterate through the actions for the current module
    forEachActions(fn) {
        if(this._raw.actions) {
            forEachValue(this._raw.actions,fn)
        }
    }
    // Iterate over the current module's getters
    forEachGetters(fn) {
        if(this._raw.getters) {
            forEachValue(this._raw.getters,fn)
        }
    }
    // Iterate over the child of the current module
    forEachChild(fn) {
        forEachValue(this._children,fn)
    }

}
Copy the code

The purpose of installing all modules is to install the methods and state of the current module into the current Store instance. Previously, we declared a Module class to create each submodule. We can use custom forEachValue in this class to traverse all mutations, actions, getters, and submodules of the current class, using the parameters as the callback of the forEachValue function. It is easy to install in a function passed in to the Store class.

Rewrite the Store class

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
let Vue
function installModule(store, rootState, path, module) {
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the object is not reactive, it will be assigned directly. If the object is reactive, the new attribute will also be reactive
        Vue.set(parent,path[path.length -1].module.state)
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // save the mutation of all modules
        this._actions = Object.create(null)     // Store actions for all modules
        this._wrappedGetters = Object.create(null)  // Store getters for all modules
        // Install all modules on the Store instance
        let state = this._modules.root.state
        // this current instance, root state, path, root module
        installModule(this, state, [], this._modules.root)

    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    commit = (type,playload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type](playload)
    }
    // 
    dispatch = (type,playload) = > {
        this._actions[type](playload)
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code

Now that we have all the methods and states of the tree installed on the Store instance, let’s add a responsive effect to the data.

resetStoreVm

The resetStoreVm method is used to cache getters by placing state on Vue instances.

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the function object is not reactive, it will be assigned directly. If the function object is reactive, the added attribute will also be reactive
        Vue.set(parent,path[path.length -1].module.state)
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)
    // Cache lazy loading by using VUE's computed
    forEachValue(wrappedGetters, (fn, key) = > {
        computed[key] = () = > {
            return fn()
        }
        / / agent
        Object.defineProperty(store.getters, key, {
            get: () = > store._vm[key]
        })
    })
    // Make the state reactive
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // save the mutation of all modules
        this._actions = Object.create(null)     // Store actions for all modules
        this._wrappedGetters = Object.create(null)  // Store getters for all modules
        // Register all modules on the Store instance
        // this current instance, root state, path, root module
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        
        // Implement state responsiveness
        resetStoreVm(this,state)

    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    commit = (type,payload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type].forEach(fn= > fn(payload))
    }
    // 
    dispatch = (type,payload) = > {
        this._actions[type].forEach(fn= > fn(payload))
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code

After putting the state on the Vue instance, let’s look at the result of the code running:It works exactly as it did in the beginning, with modules nesting implemented, but not yetNamespaced namespaceThe function. Here’s how to improve modulesnamespacedThe module.

namespaced

import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // register module [] indicates path recursive register module
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        / / {
        // _raw: rootModule,
        // _children: {}
        // state: rootModule.state
        // }
        // If the array length is, it is the root module
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // Indicates that path has a length such as [a,b]. Store this module in the children property of the root module
            let parent = path.slice(0, -1).reduce((pre, next) = > {
                // Find the parent node step by step
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]] = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // Register submodules
        if(rootModule.modules) {
            forEachValue(rootModule.modules,(moduleValue, moduleName) = >{
                // Recursively form submodules to pass in module names and module values
                this.register([...path,moduleName],moduleValue)
            })
        }
    }
    // Add new code
    getNamespaced(path) { // Get the namespace
        let root = this.root
        return path.reduce((pre, next) = >{
            //[b,c]
            // Get the submodule to see if it has the Namespaced attribute
            root = root.getChild(next)
            // If you have a namespace attribute, add it to the namespace
            return pre + (root.namespaced ? next + '/' :' ')},' ')}}Copy the code
function installModule(store, rootState, path, module) {
    // Add code to get namespace
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the function object is not reactive, it will be assigned directly. If the function object is reactive, the added attribute will also be reactive
        Vue.set(parent,path[path.length -1].module.state)
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
Copy the code
import forEachValue from "./utils"

export default class module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
    // Add code attribute accessors
    get namespaced () {
        return this._raw.namespaced
    }
    // Find the submodule of the module
    getChild(key) {
        return this._children[key]
    }
    // Append submodules to modules
    addChild(key, module) {
        this._children[key] = module
    }
    // open mutations for the current module
    forEachMutations(fn) {
        if(this._raw.mutations) {
            forEachValue(this._raw.mutations,fn)
        }
    }
    // Iterate through the actions for the current module
    forEachActions(fn) {
        if(this._raw.actions) {
            forEachValue(this._raw.actions,fn)
        }
    }
    // Iterate over the current module's getters
    forEachGetters(fn) {
        if(this._raw.getters) {
            forEachValue(this._raw.getters,fn)
        }
    }
    // Iterate over the child of the current module
    forEachChild(fn) {
        forEachValue(this._children,fn)
    }

}
Copy the code

When you implement Namespaced, you’re going to iterate over whether or not each module has one when it’s installedNamespaced attribute, and then join the paths together.

registerModule

Vuex and VuE-Router support dynamic addition of modules and routes, so we need to add a registerModule method to store to dynamically add modules.

// module-collection.js
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // register module [] indicates path recursive register module
        this.register([],options)
    }
    register(path,rootModule) {
        let newModlue = new Module(rootModule)
        / / {
        // _raw: rootModule,
        // _children: {}
        // state: rootModule.state
        // }
        // If the array length is, it is the root module
        // Map the current module instance to dynamically add modules
        rootModule.rawModule = newModlue
        if(path.length == 0) {
            this.root = newModlue
        }else {
            // Indicates that path has a length such as [a,b]. Store this module in the children property of the root module
            let parent = path.slice(0, -1).reduce((pre, next) = > {
                // Find the parent node step by step
                // return pre._children[next]
                return pre.getChild(next)
            },this.root)
            // parent._children[path[path.length-1]] = newModlue
            parent.addChild(path[path.length-1], newModlue)  
        }
        // Register submodules
        if(rootModule.modules) {
            forEachValue(rootModule.modules,(moduleValue, moduleName) = >{
                // Recursively form submodules to pass in module names and module values
                this.register([...path,moduleName],moduleValue)
            })
        }
    }
 ...
}
Copy the code

Added rootModule.rawModule = newModlue Cause: After the new Modlue module, we added an attribute mapping for the current module instance to ourselves. This is in order to dynamically register the module, we need to install the registered module on the Store instance. The installation requires the methods of traverse mutations, actions, and getters in the Module instance, but there is no Module instance in the added Module, so you need to add an attribute for the new Module when registering the Module, which maps to the current Module instance.

// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
    // Get the namespace
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the object is not reactive, it will be assigned directly. If the object is reactive, the new attribute will also be reactive
        Vue.set(parent,path[path.length -1].module.state)
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    // Store the last instance
    let oldVm = state._vm
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)

    forEachValue(wrappedGetters, (fn, key) = > {
        computed[key] = () = > {
            return fn()
        }
        / / agent
        Object.defineProperty(store.getters, key, {
            get: () = > store._vm[key]
        })
    })
    // Make the state reactive
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
    // Determine if the last vue instance exists and if so, destroy the instance during the next DOM update
    if(oldVm) {
        Vue.nextTick(() = > oldVm.$destoryed())
    }
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // save the mutation of all modules
        this._actions = Object.create(null)     // Store actions for all modules
        this._wrappedGetters = Object.create(null)  // Store getters for all modules
        // Register all modules on the Store instance
        // this current instance, root state, path, root module
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        // console.log(this._module);
        // Implement state responsiveness
        resetStoreVm(this,state)

    }
    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    commit = (type,payload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type].forEach(fn= > fn(payload))
    }
    // 
    dispatch = (type,payload) = > {
        this._actions[type].forEach(fn= > fn(payload))
    }
    
    // Add new code
    registerModule (path,rawModule) {
        // Encapsulate an array
        if(!Array.isArray(path)) path = [path]
        // Register module
        this._modules.register(path,rawModule)
        // Install the module to register the module using the method on the module instance to make a module map where the module is collected
        installModule(this.this.state,path,rawModule.rawModule)
        / / reset getters
        resetStoreVm(this.this.state)
    }
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code

After the module is installed, mutations, actions and state of the module can be used normally as other modules, but the method of getters cannot be used at present, because we process getters in the resetStoreVm method. So we need to execute the resetStoreVm function once more before we can use getters to add modules. However, we used the resetStoreVm method to create an instance of vUE when we set the module state to be responsive, and we used the resetStoreVm method to create an instance of vue when we added the module, so we have two instances of vUE. We need the nextTick method to execute the previous instance’s $deStoryed () method on the next DOM update to destroy the previous vue instance. Test results:

// store/store.js. store.registerModule(['h'] and {namespaced:true.state: {
        l: 9
    },
    getters: {
        getL(state) {
            return state.l
        }
    }
})
...
Copy the code

The test found that the root module dynamically added an H module. The dynamic registration module is completed.

Persistent plugins state

Vuex also supports the use of plug-ins internally, some of which can be registered when creating a Store.

import Vue from 'vue'
// import Vuex from '.. /vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {}const store =  new Vuex.Store({
    plugins: [
        persists
    ],
    state: {
        age: 10,}})export default store
Copy the code

When a plug-in is executed, a Store argument is provided internally, which is a Store instance, so that the desired plug-in can be written from the instance. So now the custom vuex will also function as a plug-in.

class Store{
    constructor(options){...// Store plugins
        this._subscriber = []
        // Iterate over the plug-in and execute
        options.plugins.forEach(fn= > fn(this))... }// Publish a subscription
    subscriber(fn) {
        // Automatically triggered when the state changes
        this._subscriber.push(fn)
    }
    // Update the status
    replaceState(newState) {
        this._vm._data.$$state = newState
    }
    ....
    // omit the following code
}
Copy the code
import Vue from 'vue'
import Vuex from '.. /vuex/index.js'
// import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {
    let local = localStorage.getItem('VUEX')
    if (local) {
        // Each page refresh gets the latest value from localStorage and displays it on the page
        store.replaceState(JSON.parse(local))
    }
    store.subscriber((mutations,state) = >{
        // Store the state in localStorage
        localStorage.setItem('VUEX'.JSON.stringify(state))
    }) 
}
const store =  new Vuex.Store({
    plugins: [
        persists
    ],
    state: {
        age: 10,},...// omit the following code
})

export default store
Copy the code
// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// Add code to get the latest value
function getState(store, path) {
    return path.reduce((pre, next) = > {
        return pre[next]
    },store.state)
}
function installModule(store, rootState, path, module) {
    // Get the namespace
    let namespaced  = store._modules.getNamespaced(path)
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the function object is not reactive, it will be assigned directly. If the function object is reactive, the added attribute will also be reactive
        Vue.set(parent,path[path.length -1].module.state)
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            Module. state may be the old value if it is used all the time, so you need to get the latest value
            mutations.call(store, getState(store, path), payload) // State change triggers subscriber
            // Add new code
            The first argument is an object that records the names of mutations and the trigger COMMIT. The second argument is the latest status value
            store._subscriber.forEach(sub= > sub({mutations,type},store.state))
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(getState(store, path))
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
Copy the code

We implemented a persistent state of our own through a plug-in above, where each page refresh reads the latest value in localStorage.

Distinguish between actions and mutations

In VUEX, only changes in status are allowed through mutations in strict mode, while changes in status in other cases are not allowed and errors will be reported. Here, to implement this function:

import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// Get the latest value
function getState(store, path) {
    return path.reduce((pre, next) = > {
        return pre[next]
    },store.state)
}
function installModule(store, rootState, path, module) {
    // Get the namespace
    let namespaced  = store._modules.getNamespaced(path)
    console.log(namespaced);
    // Collect the status of all modules
    if(path.length > 0) { // If it is a submodule, the state of the submodule needs to be defined to the root module
        let parent = path.slice(0, -1).reduce((pre, next) = > {
            return pre[next]
        }, rootState)
        // Add new attributes by setting properties to reactive
        // If the function object is not reactive, it will be assigned directly. If the function object is reactive, the added attribute will also be reactive
        store._WithCommitting(() = >{
            Vue.set(parent,path[path.length -1].module.state)
        })
    }
    module.forEachMutations((mutations, type) = >{
        // Collect all mutations of the module and store them on the store._mutations of the instance
        Mutations and Actions of the same name do not override the mutations, so have an array store {changeAge: [fn,fn,fn]}
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) = > {
            // Function wrapper parameter passing is flexible
            // Make this always refer to the current module state of the instance
            // Implement the plugin, where the value of state needs to be updated and the state may be replaced internally
            store._WithCommitting(() = >{
                mutations.call(store, getState(store, path), payload) // State change triggers subscriber
            })
            The first argument is an object that records the names of mutations and the trigger COMMIT. The second argument is the latest status value
            store._subscriber.forEach(sub= > sub({mutations,type},store.state))
        })
    })
    module.forEachActions((actions, type) = >{
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) = > {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) = >{
        // Compute attributes of the same name will be overridden so do not store
        store._wrappedGetters[key] = () = > {
            return getters(getState(store, path))
        }
    })
    module.forEachChild((child, key) = >{
        installModule(store, rootState, path.concat(key), child)
    })
}
function resetStoreVm (store, state) {
    let oldVm = state._vm
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)

    forEachValue(wrappedGetters, (fn, key) = > {
        computed[key] = () = > {
            return fn()
        }
        / / agent
        Object.defineProperty(store.getters, key, {
            get: () = > store._vm[key]
        })
    })
    // Make the state reactive
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
    if(oldVm) {
        Vue.nextTick(() = > oldVm.$destoryed())
    }
    // Enable strict mode
    if (store.strict) {
        store._vm.$watch(() = >store._vm._data.$$state,() = >{
            console.assert(store._committing,'Status can only be changed through mutations')
        },{deep:true.sync:true}) // Synchronization is performed as soon as the state changes}}class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
        this._mutations = Object.create(null)   // save the mutation of all modules
        this._actions = Object.create(null)     // Store actions for all modules
        this._wrappedGetters = Object.create(null)  // Store getters for all modules
        // Register all modules on the Store instance
        // this current instance, root state, path, root module
        let state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
        // console.log(this._module);
        // Implement state responsiveness
        resetStoreVm(this,state)
        this.strict = options.strict
        / / the plugin
        this._subscriber = []
        // Iterate over the plug-in and execute
        options.plugins.forEach(fn= > fn(this))
        / / synchronize Watcher
        this._committing = false
    }

    // Class property accessor This method is triggered relative to the proxy when the user obtains the instance property state from the instance
    get state() {
        $$state = this._vm._data.$$state = this._vm._data.$$state = new vue
        // This will be reactive
        return this._vm._data.$$state
    }
    Watcher can only change the status via mutations
    _WithCommitting(fn) {
        let committing = this._committing
        this._committing = true "// Slice () label _case (true) before function call ()
        fn()
        this._committing = committing
    }
    commit = (type,payload) = > {
        // triggering commit triggers the method in _mutations
        this._mutations[type].forEach(fn= > fn(payload))
    }
    // 
    dispatch = (type,payload) = > {
        this._actions[type].forEach(fn= > fn(payload))
    }

    registerModule (path,rawModule) {
        // Encapsulate an array
        if(!Array.isArray(path)) path = [path]
        // Register module
        this._modules.register(path,rawModule)
        // Install the module to register the module using the method on the module instance to make a module map where the module is collected
        installModule(this.this.state,path,rawModule.rawModule)
        / / reset getters
        resetStoreVm(this.this.state)
    }
    // Publish a subscription
    subscriber(fn) {
        // Automatically triggered when the state changes
        this._subscriber.push(fn)
    }
    // Update the status
    replaceState(newState) {
        this._WithCommitting(() = >{
            this._vm._data.$$state = newState
        })
    }
    
}
const install = (_Vue) = > {
    Vue = _Vue
    applyMinx(Vue)
}
export {
    Store,
    install
}
Copy the code

In strict mode, you need to set a _research identifier inside the Store (case), change the _research identifier to true (case) if mutations change the status (case), and set the status of all other changes to false (case). “This uses the _withresearch function (right) to label (change) _case (change), inside resetStoreVm decide whether to turn on strict mode (right), use $watch synchronization depth to listen for state changes. Determine whether the user has changed the status via mutations, based on the _research flag set up.

Helper function helper

The main part of vuex source code above has been completed, the following write vuex in the four auxiliary functions to achieve the principle.

//App.vue
<template>
  <div id="app">State.age: {{age}} getters: {{$store.getters. GetAge}} b module c {{$store.state.b.c}} D module E {{$store.state.d.e}}<button @click="$store.state.age+=10"></button>
      <button @click="$store.commit('changeAge',5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>

<script>
const mapState = (arrayList) = > {
  const obj = {}
  for(let i = 0; i < arrayList.length; i++) {
    obj[arrayList[i]] = function() {
      console.log(this);
      return this.$store.state[arrayList[i]]
    }
  }
  return obj
}
export default {
  name: 'App'.computed: {
    ...mapState(['age']),
    // age(){
    // return this.$store.state.age
    // }}}</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
Copy the code
The use of... MapState (['age']) {return this.$store.state.age}; MapGetters ([getAge]) = getAge(){return this.$store.getage}Copy the code

We know that we pass in an array and return an object, so the code looks like this:

const mapState = (arrayList) = > {
  const obj = {}
  for(let i = 0; i < arrayList.length; i++) {
    obj[arrayList[i]] = function() {
      console.log(this);
      return this.$store.state[arrayList[i]]
    }
  }
  return obj
}
Copy the code

The result is as follows: as before, the remaining three auxiliary functions can be written in this way.The code for the auxiliary function is as follows:

// helper.js
const mapState = (arrayList) = > {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function () {
            console.log(this);
            return this.$store.state[arrayList[i]]
        }
    }
    return obj
}
const mapGetters = (arrayList) = > {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function () {
            return this.$store.getters.getAge
        }
    }
    return obj
}
const mapMutations = (arrayList) = > {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function (payload) {
            return this.$store.commit(arrayList[i], payload)
        }
    }
    return obj
}
const mapActions = (arrayList) = > {
    const obj = {}
    for (let i = 0; i < arrayList.length; i++) {
        obj[arrayList[i]] = function (payload) {
            return this.$store.dispatch(arrayList[i], payload)
        }
    }
    return obj
}
export {
    mapState,
    mapGetters,
    mapActions,
    mapMutations
}
Copy the code
// App.vue
<template>
  <div id="app">State.age: {{age}} getters: {{getAge}} b module c {{$store.state.b.c}} D module e {{$store.state.d.e}<button @click="changeAge(5)">Mustation method</button>
      <button @click="$store.dispatch('changeAge',10)">Method the actions</button>
  </div>
</template>

<script>
import {mapState,mapGetters,mapMutations} from './vuex/index.js'
export default {
  name: 'App'.computed: {
    ...mapState(['age']),
    ...mapGetters(['getAge'])},methods: {
    ...mapMutations(['changeAge']),}}</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
Copy the code

The test results are as follows:

My personal understanding of Vuex

Vuex is a state management mode, through which we can communicate between components, because the install method uses vue’s mixin method to inject Store instances into each component, In the resetStoreVm method, the reactive principle of VUE itself is used to realize state responsiveness, and the cache effect of Getters is realized through computed. Then, the mutations and actions methods defined by users are stored through the idea of publishing and subscription. When the users trigger commit and dispatch, they can subscribe mutations and Actions to find out the corresponding methods. In the case of module nesting, vuex internally formats all modules into a tree using a moduleCollection class, installs the formatted tree onto the Store instance using the installModule method, and finally, The state of all modules is set to reactive by using the principle of vUE internal reactive through resetStoreVm method. Vuex is responsive and also supports the use of plug-ins. There are subscribe methods and replaceState methods in Store, through which vuex can realize the persistence of state. The above shows that VUex is highly dependent on vUE responsive and plug-in system. This is my personal understanding of VUex.

If you do not understand the Vue responsive principle, you can go to the previous article to read the Vue responsive principle

To sum up, the main part of vuex source code has been basically all over the top. The core of VUex has been basically realized. I hope it will be helpful for you to explore the core mechanism of VUex. If you have any mistakes, please correct them in the comments section. The relevant source code has been uploaded to Gitee, interested partners can go to get the source code stamp I get the source code !!!!! .