In large VUE applications, vuEX is almost always used for state management. Let’s write a VUex step by step to deepen our understanding of vuex and make it easier to use.

Implement basic usage

Let’s take a look at the most basic use of vuex.

  • store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        age: 12
    },
    mutations: {
        increment(state){
            state.age ++ 
        }
    },
})
Copy the code
  • index.js
import Vue from "vue"
import App from "./App.vue"
import store from "./store"
var vm = new Vue({
    el: '#root',
    store,
    render: h => h(App)
})
Copy the code
  • app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
    </div>
    <button @click="$store.commit('increment',2)">increment</button> 
</template>
Copy the code

Vue. Use (vuex) and new vuex.Store(options).

(2) In index.js, import the instance generated by new vuex.store (options), and pass it as an option parameter when new Vue.

(3) In app.vue, the child component instances of the VM have a $store property that can be accessed directly through this.$store.

The overall framework

According to the above functions, first write the overall framework of VUEX.

Vuex. use calls the vuex. Install method; New vuex. store indicates that vuex. store is a class.

class Store{
    ...
}

const install = (_Vue) => {
    ...
}

export default {
  install,
  Store
}
Copy the code

With the overall framework in place, the following steps will be taken to improve install and Store functionality.

Intall method

When we pass the store instance in the new Vue(options) option, it has a $store property on each of its children. Mounting the global $store is done in the install method with vue.mixin ().

Vue. Mixin (mixin). Globally register a mixin that affects all Vue instances created after registration.Copy the code
Let Vue const install = (_Vue) => {Vue = _Vue //install Mixin ({beforeCreate(){// This.$options can be used to get the parameter options passed by new Vue(options) If (this.$options.store){this.$store = this.$options.store}else{// The child $store property points to the parent $Store property $store this.$store = this.$parent && this.$parent.$store}})}Copy the code

state

We access the data through this.$store.state. The Store class has a state property.

class Store{
    constructor(options){
        this.state = options.state
    }
}
Copy the code
  • app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
    </div>
    <button @click="$store.state.age++">increment</button> 
</template>
Copy the code

$store.state.age = $store.state.age = $store.state.age; To achieve data response, VUEX uses vUE’s response mechanism.

Class Store{constructor(options){this.vm = new Vue({// Vue relies on data from data, so data from data is responsive () {return {state: Options. state}})} get state(){return this.vm.state}}Copy the code

getters

Getters is similar to computed in Vue and returns calculated values based on state.

class Store{ constructor(options){ ... Let getters = options.getters // Where objects are used for storage, object.create (null) has fewer prototype objects than {}, This.getters = object.create (null) //options Every getter passed in is a function, and the getter we want to access is a value, Similar to computed Object.keys(getters). ForEach ((getterName) => {object.defineProperty (this.getters, getterName, {get: () => { return getters[getterName](this.state) } }) }) } }Copy the code

Testing:

//store/index.js
export default new Vuex.Store({
    ...
    getters: {
        computedAge(state){
            return state.age + 10
        }
    }
})
//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <div>{{$store.getters.computedAge}}</div>
        <button @click="$store.state.age++">increment</button> 
    </div>
</template>
Copy the code

Mutation and commit

We use vuex to change data when triggering a commit method like this: this. code. store.com MIT (‘increment’,2). The increments here correspond to the types of events in mutations.

Export default new vuex. Store({state: {age: 12}, mutations: {increment(state, payload){state.age += payload}})Copy the code

In fact, this is a process of event subscription and release. Get options. Mutations, subscribe the event, and execute commit to release the event.

class Store{ constructor(options){ ... Mutations = object.keys (mutations).foreach ((type) => {let mutations = options. Mutations this. Mutations = object.create (null) // Maintain an array of subscription functions for each event (type), Every time a subscription event subscription function on the array to add const handlers = this. Mutations [type] | | (enclosing mutations [type] = []) handlers. Push (= > {(content) Mutations [type](this.state,payload)})} return = (type,payload) => { This.mutations [type].foreach ((fn) => {fn(payload)})}}Copy the code

The action and dispatch

Vuex advocates mutation as a synchronization function. To change state asynchronously, use action, triggered by the Dispatch method. (mutation is an asynchronous function that does not return an error in non-strict mode.)

//store/index.js
export default new Vuex.Store({
    state: {
        age: 12
    },
    actions: {
        incrementAsync(store,payload){
            setTimeout(() => {
                store.commit('increment',payload)
            },1000)
        }
    }
})
//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <button @click="$store.dispatch('incrementAsync',5)">incrementAsync</button> 
    </div>
</template>
Copy the code

This is also a process of subscribing and publishing events, taking options.actions, subscribing to events, and executing dispatch to publish events. The analogy is action–mutation, dispatch–commit.

class Store{ constructor(options){ ... Let actions = options.actions this.Actions = object.create (null) // Subscribe event Object.keys(actions).foreach ((type) => { // Maintain an array of subscription functions for each event (type), Every time a subscription event subscription function on the array to add const handlers = this. Actions [type] | | (enclosing the actions [type] = []) handlers. Push (= > {(content) Actions [type](this,payload) // The first parameter of the action subscription function is store})})} dispatch = (type,payload) => {// Publish events, This.actions [type].foreach ((fn) => {fn(payload)})}}Copy the code

Add the module function

Because of the use of a single state tree, all the states of an application are grouped into one large object. When the application becomes very complex, the Store object can become quite bloated.Copy the code

As projects get bigger and more people collaborate, modular data management becomes clearer.

//store/index.js
const ModuleA = {
    // namespaced: true,
    state: {
        age: 11
    },
    mutations: {
        increment(state){
            console.log('module A')
            state.age ++ 
        }
    },
    getters: {
        computedAgeAFromA(state){
            return state.age + ' aFromA'
        }
    }
}

export default new Vuex.Store({
    modules: {
        a: ModuleA
    },
    state: {
        age: 12
    },
    getters: {
        computedAge(state){
            return state.age + 10
        },
        computedAgeA(state){
           return state.a.age + 10
        }
    },
    mutations: {
        increment(state,payload){
            console.log('module root')
            state.age += payload
        }
    }
})

//app.vue
<template>
    <div id="app">
        <div>{{$store.state.age}}</div>
        <div>{{$store.getters.computedAge}}</div>
        <div>{{$store.getters.computedAgeA}}</div>
        <div>{{$store.getters.computedAgeAFromA}}</div>
        <button @click="$store.commit('increment',2)">increment</button> 
    </div>
</template>
Copy the code
  • (1) In the above example, the data age defined by module A is accessed through state.a.age, and the data age of the root module is accessed through state.age
  • (2) Getters defined in module A and root module will be directly mounted to store
  • (3) When store.mit is executed, both the A module and the mutation of the root module will be executed. (Same goes for store.dispatch.)

In other words, in addition to state, which remains a tree structure, getters, Mutations, and Actions are flattened and mounted directly on the Store.

To achieve the above module function, there are mainly two steps: 1. Create a module tree according to the options. 2. Traverse the module tree, install the module, and realize the mounting of state, getters, mutations and Actions of each module.

Creating a module tree

Vuex defines each module node in the tree as follows:

Class Module {// Module node constructor(moduleOption){this._rawModule = moduleOption // Pass the Module option this._children = Object.create(null) // The name of the module must be added to each item of the array. this.state = moduleOption.state //state } get namespaced () { return !! This._rawmodule. namespaced // Whether the module uses namespace}}Copy the code

After the Module nodes are generated based on the options, you can create the Module tree.

Export Default Class ModuleCollection {//this.root Root of the module tree Constructor (rootModuleOption) {this.register([], rootModuleOption) } register(path, moduleOption){ const module = new Module(moduleOption) if(path.length === 0){ this.root = module }else{ let parentModule  = path.slice(0,-1).reduce((module,key) => { return module._children[key] },this.root) parentModule._children[path.pop()] = module } if(moduleOption.modules){ Object.keys(moduleOption.modules).forEach( (key)  => { this.register(path.concat(key),moduleOption.modules[key]) }) } } }Copy the code

register(path, moduleOption)Path is used to locate the parent node of the current Module node. If path is an empty array, the current node is the root node. Otherwise, slice and Reduce are used to find the parent node of the current node.In contrast to the tree registration, passing a path gives each module node its name, which is used when installing the module. The subsequent namespace is also based on path.

register(parentModule, moduleOption){ const module = new Module(moduleOption) if(! ParentModule){this.root = module}else{parentModule._children.push(module)// if(moduleOption.modules){ Object.keys(moduleOption.modules).forEach( (key) => { this.register(module,moduleOption.modules[key]) }) } }Copy the code

Install the module

class Store { constructor(options){ ... this.getters = Object.create(null) this.mutations = Object.create(null) this.actions = Object.create(null) this._modules InstallModule (this,this.state,[],this._modules.root) // Install modules}... } function installModule(store,state,path,module){let rawModule = module._rawModule State = {age: 1, a: {age: 2, C: {age: 4}}, b: {age: 3 } } ***/ if(path.length > 0){ let parentState = path.slice(0,-1).reduce((state,key) => { return state[key] },state) Vue. Set (parentState,path.pop(),rawModule.state)} Object.keys(rawModule.getters).forEach((getterName) =>{ Object.defineProperty(store.getters, getterName, { get: () => {return rawModule.getterName (rawModule.state)}})}) Object.keys(rawModule.mutations).forEach((type) => { const handlers = store.mutations[type] || (store.mutations[type] = Mutations [type](rawModule. State,payload)})} if(rawModule.actions){ Object.keys(rawModule.actions).forEach((type) => { const handlers = store.actions[type] || Handlers. Push ((payload) => {rawModule.actions[type](store,payload)})})} // Recursive if(module._children){ Object.keys(module._children).forEach((key) => { installModule(store,state,path.concat(key),module._children[key]) }) } }Copy the code

Add the namespace

Without namespace, when store.mit is executed, both the A module and the mutation of the root module will be executed. To avoid repeated execution, you need to check to see if the mutation name already exists in another module before writing the mutation name in the module. Using namespace can avoid this problem. You need to configure namespaced: true in the Module

const ModuleA = {
    namespaced: true,
    state: {},
    mutations: {}
}
Copy the code

A namespace is a unique identifier for each module. Vuex uses the access path of a module node as the namespace of a module.

function getNamespace(root,path){ 
    let module = root
    return path.reduce((namespace, key) => {
      module = module._children[key]
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}
//path=[] --> ''
//path=['a'] --> 'a/'
//path=['a','c'] --> 'a/c/'
Copy the code

Once you have a namespace, add the namespace when you install getters, mutations, and actions.

function installModule(store,state,path,module){ ... let namespace = getNamespace(store._modules.root,path) if(rawModule.getters){ Keys (rawModule.getters).foreach ((getterName) =>{let namespacedGetter = namespace + getterName //<----- here Object.defineProperty(store.getters, namespacedGetter, { get: () => { return rawModule.getters[getterName](rawModule.state) } }) }) } if(rawModule.mutations){ Keys (rawModule. Mutations). ForEach ((type) => {let namespacedType = namespace + type //<----- here const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = []) handlers.push((payload) => { rawModule.mutations[type](rawModule.state,payload) }) }) } if(rawModule.actions){ Object. Keys (rawModule.actions). ForEach ((type) => {let namespacedType = namespace + type //<----- const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = []) handlers.push((payload) => { rawModule.actions[type](store,payload) }) }) } ... }Copy the code

The source code

  • vuex/index.js
import ModuleCollection from "./module-collection.js"
let Vue

function getNamespace(root,path){ 
    let module = root
    return path.reduce((namespace, key) => {
      module = module._children[key]
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}

function installModule(store,state,path,module){
    let rawModule = module._rawModule
    let namespace = getNamespace(store._modules.root,path)
    if(path.length > 0){
        let parentState = path.slice(0,-1).reduce((state,key) => {
            return state[key]
        },state)
        Vue.set(parentState,path.pop(),rawModule.state)
    }
    if(rawModule.getters){
        Object.keys(rawModule.getters).forEach((getterName) =>{
            let namespacedGetter = namespace + getterName
            Object.defineProperty(store.getters, namespacedGetter, {
                get: () => {
                    return rawModule.getters[getterName](rawModule.state)
                }
            })
        })
    }
    if(rawModule.mutations){
        Object.keys(rawModule.mutations).forEach((type) => {
            let namespacedType = namespace + type
            const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.mutations[type](rawModule.state,payload)
            })
        })
    }
    if(rawModule.actions){
        Object.keys(rawModule.actions).forEach((type) => {
            let namespacedType = namespace + type
            const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = [])
            handlers.push((payload) => {
                rawModule.actions[type](store,payload)
            })
        })
    }
    
    if(module._children){
        Object.keys(module._children).forEach((key) => {
            installModule(store,state,path.concat(key),module._children[key])
        })
    }
}
class Store {
    constructor(options){
        this.vm = new Vue({
            data() {
                return {
                    state: options.state
                }
            }
        })


        this.getters = Object.create(null)
        this.mutations = Object.create(null)
        this.actions = Object.create(null)

        this._modules = new ModuleCollection(options)
        installModule(this,this.state,[],this._modules.root)
        console.log(this)
    }
    get state(){
        return this.vm.state
    }

    commit = (type,payload) => {
        this.mutations[type].forEach((fn) => {
            fn(payload)
        })
    }

    dispatch = (type,payload) => {
        this.actions[type].forEach((fn) => {
            fn(payload)
        })
    }
}

const install = (_Vue) => {
    Vue = _Vue

    Vue.mixin({
        beforeCreate(){
            if(this.$options.store){
                this.$store = this.$options.store
            }else{
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}

export default {
    install,
    Store
}
Copy the code
  • vuex/module-collection.js
Class Module {// Module node constructor(moduleOption){this._rawModule = moduleOption this._children = object.create (null)  this.state = moduleOption.state } get namespaced () { return !! This._rawmodule. namespaced}} export default class ModuleCollection {//this.root Root of the module tree (rootModuleOption) { this.register([], rootModuleOption) } register(path, moduleOption){ const module = new Module(moduleOption) if(path.length === 0){ this.root = module }else{ let parentModule  = path.slice(0,-1).reduce((module,key) => { return module._children[key] },this.root) parentModule._children[path.pop()] = module } if(moduleOption.modules){ Object.keys(moduleOption.modules).forEach( (key)  => { this.register(path.concat(key),moduleOption.modules[key]) }) } } }Copy the code

Vue source code series article:

The responsive principle of VUe2.0

Vue compilation process analysis

Vuex principle from shallow to deep handwriting vuex

The VUE component goes from building a VNode to generating a real tree of nodes