Vuex is a vUe-specific state management tool. The internal implementation is forcibly bound to vUE. In a project, it is often used to manage the state of global or multiple uncertain components, such as token of login status, user information, etc.

After vuE3 came out, Vuex also made corresponding upgrades, mainly in the responsive aspect.

The following content is based on the [email protected] version for vue3.x version

use

Initialize the

// main.js
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";

createApp(App).use(store).mount("#app");

// store.js
import { createStore } from "vuex";

export default createStore({
  strict: false.state: {
    num: 0,},getters: {
    getCount(state) {
      returnstate.num; }},mutations: {
    addM(state){ state.num++; }},actions: {
    add(store) {
      store.commit('addM'); }},modules: {
    a: {}}});Copy the code

Refer to the store

// vuex uses provide to pass store instances. You can useStore directly through useStore.
$store cannot be retrieved directly because setup does not have this
// It can also be imported using import store from 'store.js'
import { useStore } from 'vuex';
export default {
  setup() {
    let store = useStore();
    function add() {
      store.dispatch('add');
    }
    return {
      add
    }
  }
}
Copy the code

The flow chart

Vuex maintains a global state object, which is handled in a responsive manner. When state is changed by commit (or a Commit is committed by Dispatch), the data response is triggered to update the dependent components.

The main function points are state, mutaion, Action, Modules, and Plugin. Or we could have strict. How to use the vuex website => vuex website

Here are some of the core implementations.

The core function

Initialize the

class Store {
  constructor (options = {}) {
    const {
      plugins = [],
      strict = false,
      devtools
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._makeLocalGettersCache = Object.create(null)
    this._devtools = devtools

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    // Bind dispatch's this with call
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    // Bind commit's this with call
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state
    
    / / processing module
    installModule(this, state, [], this._modules.root)
    
    // Handle store state
    resetStoreState(this, state)

    // Execute the plug-in
    plugins.forEach(plugin= > plugin(this))}}Copy the code

Initialization does the following:

  • Initialize various states, including action collection, mutatuon collection, getter collection, and so on
  • Create and install modules, which deal with modules, as described below
  • Dealing with the state of the store, which is the heart of it, is responding to the state and getter
  • Execute the plug-in, enter the store instance, through the plug-in to do the listening and processing of data

module

This step provides the following functions:

  • Copy the contents of the Module into the outermost state, as shown{state: {a: {state: {xxx}}}}So we can passthis.$store.state.a.xxxGet the state in the module
  • Maintain all mutatuon globally_mutationsIn the
  • Maintain all actions globally_actionsIn the
  • Maintain all getters globally_wrappedGettersIn the

Creating a Collection of Modules

class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  get (path) {
    return path.reduce((module, key) = > {
      return module.getChild(key)
    }, this.root)
  }

  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) = > {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : ' ')},' ')
  }
  
  register (path, rawModule, runtime = true) {
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) = > {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}
Copy the code

This recursively generates a Module object for each module, which maintains a children for the module’s children. Store the key value of the Module into the path object and recurse to each module. The final data structure is as follows:

{
    root: {
        _rawModule: {},
        _children: {
            a: {
                _rawModule: {},
                _children: {
                    a1: moduleA1,
                    a2: moduleA2
                }
            },
            b: {
                 _rawModule: {},
                _children: {
                    b1: moduleB1,
                    b2: moduleB2
                }
            }
        }
    }
}
Copy the code

Here’s a good example of a process to learn, which is the operation to get parent and recurse down through reduce.

const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
Copy the code

For example, if the hierarchy is A -> B -> C -> D, B is the module of A, C is the module of B, and D is the module of C. To add D to the children of C, the path is [a, B, C, D], and then search down layer by layer through reduce to find the module of C, elegant ~ :

get (path) {
    return path.reduce((module, key) = > {
      return module.getChild(key)
    }, this.root)
  }
Copy the code

Install the module

There are two main things done here:

  • Put the module’s state into the parent module’s state in the format:state: { a: {state: {a1: {state: {}}}}}, hierarchy nesting. So we can get the state of module A by calling this.$store.a.state.If the module name is the same as the parent module’s state name, the state property will be overwritten, so be careful about naming ⚠️
  • Getters, mutation, and actions are maintained in the global map. If namespaced is true, the key in the global map will be added to the module’s key, for example:
{
    modules: {
        a: {
            namespaced: true.getters: {getA: () = > {}},
            modules: {
                a1: {
                    namespaced: true.getters: {getA1: () = >{}}},a2: {
                    getters: {getA2: () = >{}}}}},b: {
            getters: {getB: () = > {}},
            modules: {
                b1: {
                    getters: {getB1: () = >{}}},b2: {
                    getters: {getB2: () = > {}},
                }
            }
        }
    }
}

=>

getters: {
    a/getA: () = > {},
    a/a1/getA1: () = > {},
    a/getA2: () = > {},
    getB1: () = > {},
    getB2: () = >{},}Copy the code

So in the case of modules, pay attention to module and internal attribute naming.

State data reactive processing

After the Module is processed, reactive handling of state is required, primarily in the resetStoreState method

function resetStoreState (store, state, hot) {
  const oldState = store._state

  store.getters = {}
  store._makeLocalGettersCache = Object.create(null)
  // This is the global getter collection extracted when the module is installed
  const wrappedGetters = store._wrappedGetters
  const computedObj = {}
  forEachValue(wrappedGetters, (fn, key) = > {
    computedObj[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () = > computedObj[key](),
      enumerable: true // for local getters
    })
  })

  store._state = reactive({
    data: state
  })
}
Copy the code
  • throughreactiveState’s data is processed responsively
  • Pass the value of global gettersObject.definePropertyMap tostore.getterson

When vue obtains the state value of store from the effect function, it puts the component instance into the track dependency. When COMMIT changes the state, it triggers the update of the dependency

commit

  • Execute the function matched in mutation
  • Execute subscription function
commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    // Get the matching mutation from the global
    const entry = this._mutations[type]
    if(! entry) {return
    }
    this._withCommit(() = > {
    // start the mutation function
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    
    // The subscriber is executed. Devtool can use the subscription to listen for changes in data every time the state changes and keep a record of the data flow
    this._subscribers
      .slice()
      .forEach(sub= > sub(mutation, this.state))
  }
Copy the code

dispatch

Dispatch is similar to COMMIT in that the return of an action is wrapped in a promise and returned at resolve, so asynchronous functions can be performed at Dispatch

Similarly, the action’s subscription function is executed before the action is executed

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

    const action = { type, payload }
    const entry = this._actions[type]
    if(! entry) {return
    }

    try {
    // Execute the subscribe function for the action. Devtool subscribes mainly through the subscribe method
      this._actionSubscribers
        .slice()
        .filter(sub= > sub.before)
        .forEach(sub= > sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }
    
    const result = entry.length > 1
      ? Promise.all(entry.map(handler= > handler(payload)))
      : entry[0](payload)
    // Package result in a promise, so asynchronous functions can be performed in dispatch
    return new Promise((resolve, reject) = > {
      result.then(res= > {
        try {
          this._actionSubscribers
            .filter(sub= > sub.after)
            .forEach(sub= > sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error= > {
        try {
          this._actionSubscribers
            .filter(sub= > sub.error)
            .forEach(sub= > sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }
Copy the code

Why does VUex not recommend directly changing state

You can’t just change the state in the store. The only way to change the state in a store is to commit mutation explicitly. This allows us to easily track each state change, which allows us to implement tools that help us better understand our application.

This is not mandatory. If we change state directly, it will work.

Simply commit through action, change state through COMMIT, and follow the state management process to allow some tools to fully track the flow of data. For example, as described in commit and Dispatch above, changes to data are accompanied by a subscription function that notifies the status change.

How does VUex know that data is not committed via commit

Do not mutate vuex store state outside mutation handlers. Do not mutate vuex store state outside mutation handlers.

When a commit is performed, the mutation methods are placed in _withCommit,

 // commit
this._withCommit(() = > {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}
Copy the code

If STRICT is on in VUEX, the state changes are monitored through Watch.

function enableStrictMode (store) {
  watch(() = > store._state.data, () = > {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)}}, {deep: true.flush: 'sync'})}Copy the code

If the data changes and Commiting is not true at this point, a prompt is issued.

The difference between vue3. X and vue2

Vue3. X began to rely on the [email protected] version, mainly due to inconsistency in the handling of data responses

Before [email protected], the responsiveness of state is handled by new Vue. When Vue obtains the value of state, the current component instance is stored in the dependency of DEPS, and the data is changed to implement the dependency.

function resetStoreVM (store, state, hot) {
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) = > {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () = > store._vm[key],
      enumerable: true // for local getters})})// 
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

  if (store.strict) {
    enableStrictMode(store)
  }
}
Copy the code

There is also a change in the handling of getters. In Vex4. x, instead of using computed to wrap getters, you execute methods in getters directly in functions and get the latest in real time when render executes.

conclusion

  1. The module handles getters, mutations, and actions globally, and namespaced: true. If namespaced: true, the module’s key is added to the global object’s key"a/b/getCount"
  2. Reactive establishes reactive state and binds vUE
  3. Dispatch wraps execution results in promises, so Dispatch can execute asynchronous functions
  4. Both commit and Dispatch execute the functions in the subscription list before executing the matching method, which is why devTool can track changes in data flow, so it is recommended to change state through commit rather than directly changing or state