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.xxx
Get the state in the module - Maintain all mutatuon globally
_mutations
In the - Maintain all actions globally
_actions
In the - Maintain all getters globally
_wrappedGetters
In 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
- through
reactive
State’s data is processed responsively - Pass the value of global getters
Object.defineProperty
Map tostore.getters
on
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
- 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"
- Reactive establishes reactive state and binds vUE
- Dispatch wraps execution results in promises, so Dispatch can execute asynchronous functions
- 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