preface
In today’s front-end engineering development, VUEX and REDUx have become the best choice for state management in our projects. As for how to use it, I believe it has become a must-have skill for front-end developers. Today, we are going to make a further attempt to implement a status manager to manage our project, so that we can locate problems more quickly in the future development process. We can ask the interviewer (Hello, can you describe the implementation principle of VUEX?) You can answer these questions more calmly.
It takes about 28 minutes to read this article with over 4000 words. If there are any deficiencies, please correct them
The actual use
I believe most of you will use Vuex this way in your daily development
// store.js
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
text: "Hello Vuex"
},
getters: {},
mutations: {},
actions: {},
modules: {}
)}
Copy the code
Sharpening the knife does not mistakenly cut wood workers, simple analysis of vuex
We mainly did the following two steps after introducing VUEX
-
Vue.use(Vuex)
Here is our description
vuex
One has to be exposedinstall
Method, this oneinstall
Methods can help us invue
Register our functionality on the prototype. -
new Vuex.Store()
As the name implies, vuex not only exposes the install method, but also exposes a Store class that holds state, muations, actions, getters, commit, dispatch, etc
Start building your own VUex
To realize the vue. Use
From the brief analysis above we can see that we need to create a install function and a store class, and then expose it
New my – vuex. Js
// my-vuex.js
letVue const install = _Vue => {// vue.use() const install = _Vue => {// vue.use() const install = _Vue => {// Vue.export default {
install,
Store
}
Copy the code
Now that the basic vuex structure is in place, let’s continue to refine the Install function. The install function should be a procedure for mounting the global $store.
// my-vuex.js
letVue const install = _Vue => {// Vue. Use () is a variable that receives Vue = _Vue // Vue. Mixin helps us mix globally$store
Vue.mixin({
beforeCreate(){// This refers to vue instance const options = this.$options
if(options.store){// Determine whether the current component defines a store, and if so, use the internal store this.$store = typeof options.store === 'function' ? options.store() : options.store
} else if(options.parent && options.parent.$store){// If there is no store defined inside the component, it inherits from the parent component$storeThis method.$store = options.parent.$store
}
}
})
}
class Store {
}
export default {
install,
Store
}
Copy the code
We have already injected the $store instance into vue via vue.use, now we can continue to improve the functionality in store
To implement the state
We usually use this.$store.state to get data in components, so here we need to define the method to get state on the Store class
The code for my-vuex.js is as follows
Class Store {constructor(options={}){this.options = options} getstate() {return this.options.state
}
}
export default {
install,
Store
}
Copy the code
Test the
store.js
// store.js
import Vue from "vue"
import Vuex from "./my-vuex.js"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
text: "Hello Vuex"
},
getters: {},
mutations: {},
actions: {},
modules: {}
})
Copy the code
App.vue
<template>
<div id="app">
<h1>{{getState}}</h1>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.state.text
}
}
}
</script>
Copy the code
When you run the code, you will see that the expected Hello Vuex is displayed
But there is a small problem here. We all know that vUE data is responsive. If we proceed as follows:
// App.vue
<template>
<div id="app">
<h1>{{getState}}</h1>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.state.text
}
},
mounted() {setTimeout(() => {
console.log('Executed')
this.$store.state.text = 'haha'
}, 1000)
}
}
</script>
Copy the code
When the code runs, we’ll see that the data on the page hasn’t changed, so we’ll change the state to be responsive. There are two ways to do this
- using
vue
self-provideddata
Responsive mechanism
// my-vuex.js class Store {constructor(options={}){this.options = options this.vmData = new Vue({data: { state: options.state } }); } getstate() {return this.vmData._data.state
}
}
Copy the code
- using
vue
Server addedVue.observable()
implementation
// my-vuex.js class Store {constructor(options={}){this.options = options this.vmdata ={ state:Vue.observable(options.state || {}) } } getstate() {return this.vmData.state
}
}
Copy the code
Implement getters
The code for my-vuex.js is as follows
// my-vuex.js class Store {constructor(options={}){this.options = options this.vmdata ={ The state: the Vue observables (options. The state | | {})} / / initializes the getters enclosing getters = {} / / traverse the getters on the store Object.defineproperty (this.getters,key,{object.defineProperty (this.getters,key,}).defineProperty(this.defineters,key,{ get:()=>{return options.getters[key](this.vmData.state)
}
})
})
}
get state() {return this.vmData.state
}
}
Copy the code
Test the
store.js
import Vue from "vue"
import Vuex from "./my-vuex.js"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
text: "Hello Vuex"
},
getters: {
getText(state){
return state.text
}
},
mutations: {},
actions: {},
modules: {}
})
Copy the code
App.vue
<template>
<div id="app">
<h1>{{getState}}</h1>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.getters.getText
}
}
}
</script>
Copy the code
Implement the mutation and commit methods
The code for my-vuex.js is as follows
// omit redundant code
class Store {
constructor(options={}){
this.options = options
this.vmData = {
state:Vue.observable(options.state || {})
}
// Initialize getters
this.getters = {}
// Iterate over getters on store
Object.keys(options.getters).forEach(key= >{
// Define get operations for all functions in getters
Object.defineProperty(this.getters,key,{
get:(a)= >{
return options.getters[key](this.vmData.state)
}
})
})
// Initialize mutations
this.mutations = {}
// Go through all the functions in mutations
Object.keys(options.mutations).forEach(key= >{
// Copy the assignment
this.mutations[key] = payload= >{
options.mutations[key](this.vmData.state,payload)
}
})
Commit is essentially implementing the function specified on mutations
this.commit = (type,param) = >{
this.mutations[type](param)
}
}
get state(){
return this.vmData.state
}
}
Copy the code
Test the
store.js
import Vue from "vue"
import Vuex from "./my-vuex.js"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
text: "Hello Vuex"
},
getters: {
getText(state){
return state.text
}
},
mutations: {
syncSetText(state,param){
state.text = param
}
},
actions: {},
modules: {}
})
Copy the code
App.vue
<template>
<div id="app">
<h1>{{getState}}</h1>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.getters.getText
}
},
mounted() {setTimeout(() => {
console.log('Executed')
this.$store.commit('syncSetText'.'Synchronize change data')
}, 1000)
}
}
</script>
Copy the code
Implement the Action and Dispatch methods
The principle of Action and mutations is similar, and the implementation method of Dispatch is similar to commit
The code for my-vuex.js is as follows
Constructor (options={}){this.options = options this.vmData ={constructor(options={}){this.options = options this. The state: the Vue observables (options. The state | | {})} / / initializes the getters enclosing getters = {} / / traverse the getters on the store Object.defineproperty (this.getters,key,{object.defineProperty (this.getters,key,}).defineProperty(this.defineters,key,{ get:()=>{returnReturn mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations = {return mutations Mutations [key] = payload=>{ Options. Mutations [key] (this) vmData) state, payload)}}) / / commit is actually perform the function specified in the mutations this.com MIT = (type,param)=>{
this.mutations[typeActions = {} object.keys (options.actions). ForEach (key => {this.actions[key] = payload => { options.actions[key](this, payload) } }) this.dispatch = (type,param)=>{
this.actions[type](param)
}
}
get state() {return this.vmData.state
}
}
Copy the code
Test the
store.js
import Vue from "vue"
import Vuex from "./my-vuex.js"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
text: "Hello Vuex"
},
getters: {
getText(state){
return state.text
}
},
mutations: {
syncSetText(state,param){
state.text = param
}
},
actions: {
asyncSetText({commit},param){
commit('syncSetText',param)
}
},
modules: {}
})
Copy the code
App.vue
<template>
<div id="app">
<h1>{{getState}}</h1>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.getters.getText
}
},
mounted() {setTimeout(() => {
console.log('Executed')
this.$store.dispatch('asyncSetText'.'Asynchronously changing data')
}, 1000)
}
}
</script>
Copy the code
Simplify your code
At present, the basic functions of VUEX have been implemented, but the above code is a little redundant, let’s optimize it, mainly from the following two points
1. Encapsulate multiple occurrences of Object.keys().foreach () into a common forEachValue function
function forEachValue (obj, fn) {
Object.keys(obj).forEach(key=>fn(obj[key], key));
}
Copy the code
2. Encapsulate multiple initialized reassignment parts into readable register functions
The optimized code looks like this
// my-vuex.js class Store {constructor(options={}){this.options = options this.vmdata ={ The state: the Vue observables (options. The state | | {})} / / initializes the getters enclosing getters = {}forEachValue(options.getters,(getterFn,getterName)=>{registerGetter(this,getterName,getterFn)} this.mutations = {}forEachValue(options.mutations,(mutationFn,mutationName)=>{ registerMutation(this,mutationName,mutationFn) } ) // Initialize actions this.actions = {}forEachValue(options.actions,(actionFn,actionName)=>{ registerAction(this,actionName,actionFn) } ) // Commit is essentially executing the function specified on mutations this.com MIT = (type,param)=>{
this.mutations[type](param)
}
this.dispatch = (type,param)=>{
this.actions[type](param)
}
}
get state() {returnThis.vmdata.state}} // Register the getterfunction registerGetter(store,getterName,getterFn){
Object.defineProperty(store.getters,getterName,{
get:()=>{
returnGetterfn. call(store, store.vmdata.state)}})} // register mutationfunctionregisterMutation(store,mutationName,mutationFn){ store.mutations[mutationName] = payload=>{ MutationFn. Call (store, store. VmData. State, payload)}} / / registered actionfunctionregisterAction(store,actionName,actionFn){ store.actions[actionName] = payload=>{ actionFn.call(store,store,payload) } } // Encapsulates a common loop execution functionfunction forEachValue (obj, fn) {
Object.keys(obj).forEach(key=>fn(obj[key], key));
}
export default {
install,
Store
}
Copy the code
Implement module modularization
As our project becomes more and more complex, module will be introduced for modular state management. Let’s continue to implement module functions
How do we use modules in general
The code for store.js is as follows
import Vue from "vue"
// import Vuex from "./my-vuex.js"
import Vuex from "vuex"
Vue.use(Vuex)
let moduleA = {
state:{
nameA:'I'm module A'
},
mutations:{
syncSetA(state,param){
state.nameA = param
}
},
actions:{
asyncSetState({commit},param){
setTimeout(()=>{
commit('syncSetA',param)
},1000)
}
},
getters:{
getA(state){
return state.nameA
}
}
}
let moduleB = {
state:{
nameB:'I'm module B'
},
mutations:{
syncSetB(state,param){
state.nameB = param
}
},
actions:{
asyncSetState({commit},param){
setTimeout(()=>{
commit('syncSetB',param)
},1000)
}
},
getters:{
getB(state){
return state.nameB
}
}
}
export default new Vuex.Store({
modules:{
moduleA,moduleB
},
state: {
text: "Hello Vuex"
},
getters: {
getText(state){
return state.text
}
},
mutations: {
syncSetText(state,param){
state.text = param
}
},
actions: {
asyncSetText({commit},param){
commit('syncSetText',param)
}
}
})
Copy the code
The code for app.vue is as follows
<template>
<div id="app">
<h1>{{getState}}</h1>
A<h2>{{stateA}}</h2>
B<h2>{{stateB}}</h2>
</div>
</template>
<script>
export default{
computed:{
getState() {return this.$store.getters.getText
},
stateA() {return this.$store.state.moduleA.nameA
},
stateB() {return this.$store.state.moduleB.nameB
}
},
mounted() {setTimeout(() => {
this.$store.dispatch('asyncSetState'.'Asynchronously changing data')
}, 1000)
}
}
</script>
Copy the code
In the case of not enable nameSpace, we found that we get modules within the state to use this. $store. State. ModuleB. Access to nameA way. Mutations or action in the triggering module is the same as before, but all mutations or action with the same name in two different modules need to be implemented. The following two steps are used for modular implementation
1. The formatmodules
Incoming data
If our store.js looks like this
export default new Vuex.Store({
modules:{
moduleA,moduleB
},
state: {},
getters: {},
mutations: {},
actions: {}
})
Copy the code
We can format this to form a module state tree
Const newModule = {// root module store _rootModule:store, // children:{moduleA:{_rootModule:moduleA, _children:{}, state:moduleA.state }, moduleB:{ _rootModule:moduleB, _children:{}, state:moduleB.state } }, // Root module status state:store.state}Copy the code
To do this, we need to add a moduleCollection class that collects data from store.js and formats it into a state tree
The code for my-vuex.js is as follows
// my-vuex.js
let Vue
const install = _Vue= > {
// Omit some code
}
class Store {
constructor(options={}){
// Omit some code
// Format data to generate a state tree
this._modules = new ModuleCollection(options)
}
}
class moduleCollection{
constructor(rootModule){
this.register([],rootModule)
}
register(path,rootModule){
const newModule = {
_rootModule:rootModule, / / root module
_children:{}, / / modules
state:rootModule.state // The root module status
}
// The length of path is 0, indicating that the root element initializes data
if(path.length === 0) {this.root = newModule
}else{
// Reduce can quickly convert flat data into tree data
const parent = path.slice(0.- 1).reduce((module,key) = >{
return module._children(key)
},this.root)
parent._children[path[path.length - 1]] = newModule
}
// If it contains modules, the internal module needs to be registered in a loop
if(rootModule.modules){
forEachValue(rootModule.modules,(rootChildModule,key)=>{
this.register(path.concat(key),rootChildModule)
})
}
}}
Copy the code
2. Install the status tree
The data in store.js has been recursively assembled into a state tree, and we need to install the state tree into the Store class. There are two major changes here
-
– Added the installModule function, which helps us register the formatted state tree in the Store class
-
The registry functions (registerMutation, registerGetter, etc.) and trigger functions (commit, dispatch) have been reworked.
The code for my-vuex.js is as follows
// my-vuex.js
// Omit some code
class Store {
constructor(options={}){
this.options = options
// Initialize getters
this.getters = {}
// Initialize mutations
this.mutations = {}
// Initialize actions
this.actions = {}
// Initialize data to generate a state tree
this._modules = new moduleCollection(options)
this.commit = (type,param) = >{
this.mutations[type].forEach(fn= >fn(param))
}
this.dispatch = (type,param) = >{
this.actions[type].forEach(fn= >fn(param))
}
const state = options.state;
const path = []; // The initial path is empty to the root path
installModule(this, state, path, this._modules.root);
this.vmData = {
state:Vue.observable(options.state || {})
}
}
get state(){
return this.vmData.state
}
}
class moduleCollection{
// Omit some code
}
/ / recursive state tree, mount getters, actions, and mutations
function installModule(store, rootState, path, rootModule) {
// Here we loop out the state in the module and set it to the root state so that we can access the data through this.$store.state.modulea
if (path.length > 0) {
const parent = path.slice(0.- 1).reduce((state,key) = >{
return state[key]
},rootState)
Vue.set(parent, path[path.length - 1], rootModule.state)
}
// Cyclic registration contains all getters in the module
let getters = rootModule._rootModule.getters
if (getters) {
forEachValue(getters, (getterFn, getterName) => {
registerGetter(store, getterName, getterFn, rootModule);
});
}
// Loop registration includes all mutations in the module
let mutations = rootModule._rootModule.mutations
if (mutations) {
forEachValue(mutations, (mutationFn, mutationName) => {
registerMutation(store, mutationName, mutationFn, rootModule)
});
}
// Loop registration contains all actions within the module
let actions = rootModule._rootModule.actions
if (actions) {
forEachValue(actions, (actionFn, actionName) => {
registerAction(store, actionName, actionFn, rootModule);
});
}
// If modules nested modules, recursive installation is required
forEachValue(rootModule._children, (child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
// The state in getters here is the state in the respective module
function registerGetter(store,getterName,getterFn,currentModule){
Object.defineProperty(store.getters,getterName,{
get:(a)= >{
return getterFn.call(store,currentModule.state)
}
})
}
// Due to the mutation duplication in each module, publish-subscribe mode was used for registration
function registerMutation(store,mutationName,mutationFn,currentModule){
let mutationArr = store.mutations[mutationName] || (store.mutations[mutationName] = []);
mutationArr.push((payload) = >{
mutationFn.call(store,currentModule.state,payload)
})
}
function registerAction(store,actionName,actionFn){
let actionArr = store.actions[actionName] || (store.actions[actionName] = []);
actionArr.push((payload) = >{
actionFn.call(store,store,payload)
})
}
// Omit the rest of the code
Copy the code
So far, we have realized the basic functions of Vuex. Of course, other functions such as nameSpace, plugins,store. Subscribe are not expanded here. Here is a suggestion for small partners to clarify their thinking first. What is vuex and what functions are to be implemented? How can it be better achieved? If you’re thinking straight, I think you can write a better vuex
Auxiliary functions in VUEX are includedmapState,mapGetters,mapMutations,mapActions
The implementation of the
The realization principle of auxiliary function is relatively simple, we try by ourselves
const mapState = stateList => {
return stateList.reduce((prev,stateName)=>{
prev[stateName] =function() {return this.$store.state[stateName]
}
return prev
},{})
}
const mapGetters = gettersList => {
return gettersList.reduce((prev,gettersName)=>{
prev[gettersName] =function() {return this.$store.getters[gettersName]
}
return prev
},{})
}
const mapMutations = mutationsList => {
return mutationsList.reduce((prev,mutationsName)=>{
prev[mutationsName] =function(payload){
return this.$store.commit(mutationsName,payload)
}
return prev
},{})
}
const mapActions = actionsList => {
return actionsList.reduce((prev,actionsName)=>{
prev[actionsName] =function(payload){
return this.$store.dispatch(actionsName,payload)
}
return prev
},{})
}
Copy the code
Complete code for this article
// my-vuex.js
letVue const install = _Vue => {// Vue. Use () is a variable that receives Vue = _Vue // Vue. Mixin helps us mix globally$store
Vue.mixin({
beforeCreate(){// This refers to vue instance const options = this.$options
if(options.store){// Determine whether the current component defines a store, and if so, use the internal store this.$store = typeof options.store === 'function' ? options.store() : options.store
} else if(options.parent && options.parent.$store){// If there is no store defined inside the component, it inherits from the parent component$storeThis method.$store = options.parent.$storeThis.options = this.options = this.options = this.options = this.options = this.options = this.options = this.options = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results = this.results This. mutations = {} // Initialize actions this.actions = {} // initialize the data and generate a state tree this._modules = new moduleCollection(options) // Commit is essentially executing the function specified on mutations this.com MIT = (type,param)=>{
this.mutations[type].forEach(fn=>fn(param))
}
this.dispatch = (type,param)=>{
this.actions[type].forEach(fn=>fn(param)) } const state = options.state; const path = []; InstallModule (this, state, path, this._modules. Root); this.vmData = { state:Vue.observable(options.state || {}) } } getstate() {returnModuleCollection {constructor(rootModule){this.register([],rootModule)} Register (path,rootModule){const newModule = {_rootModule:rootModule, // rootModule _children:{}, // submodule state: rootmodule. state // rootModule state} // if the length of path is 0, it indicates that the root element initializes dataif(path.length === 0){
this.root = newModule
}elseConst parent = path.slice(0,-1).reduce((module,key)=>{// Reduce can quickly convert flat data into tree data const parent = path.slice(0,-1).reduce((module,key)=>{// Reduce can quickly convert flat data into tree data const parent = path.slice(0,-1).reduce((module,key)=>{returnModule._children [key]},this.root) parent._children[path[path.length-1]] = newModule} // If modules exist, internal modules need to be registeredif(rootModule.modules){
forEachValue(rootModule.modules,(rootChildModule,key)=>{ this.register(path.concat(key),rootChildModule) }) } }} // Recursive state tree, mount getters, actions, mutationsfunctionInstallModule (Store, rootState, path, rootModule) {$store.state.modulea to access dataif (path.length > 0) {
const parent = path.slice(0,-1).reduce((state,key)=>{
returnState [key]},rootState) vue.set (parent, path[path.length-1], rootModule.state)} // Cycle register all getters in the modulelet getters = rootModule._rootModule.getters
if (getters) {
forEachValue(getters, (getterFn, getterName) => { registerGetter(store, getterName, getterFn, rootModule); }); } // Loop registration includes all mutations in the modulelet mutations = rootModule._rootModule.mutations
if (mutations) {
forEachValue(mutations, (mutationFn, mutationName) => { registerMutation(store, mutationName, mutationFn, rootModule) }); } // Loop registration contains all actions within the modulelet actions = rootModule._rootModule.actions
if (actions) {
forEachValue(actions, (actionFn, actionName) => { registerAction(store, actionName, actionFn, rootModule); }); } // If modules nested modules, recursive installation is requiredforEachValue(rootModule._children, (child, key) => { installModule(store, rootState, path.concat(key), Child)})} // The state in the getters here is the state in the respective modulefunction registerGetter(store,getterName,getterFn,currentModule){
Object.defineProperty(store.getters,getterName,{
get:()=>{
returnGetterFn. Call (store, currentModule. State)}})} / / due to the presence of repeat each module mutation, so use publish-subscribe pattern to register herefunction registerMutation(store,mutationName,mutationFn,currentModule){
let mutationArr = store.mutations[mutationName] || (store.mutations[mutationName] = []);
mutationArr.push((payload)=>{
mutationFn.call(store,currentModule.state,payload)
})
}
function registerAction(store,actionName,actionFn){
let actionArr = store.actions[actionName] || (store.actions[actionName] = []);
actionArr.push((payload)=>{
actionFn.call(store,store,payload)
})
}
function forEachValue (obj, fn) { Object.keys(obj).forEach(key=>fn(obj[key], key)); } // Auxiliary functionsexport const mapState = stateList => {
return stateList.reduce((prev,stateName)=>{
prev[stateName] =function() {return this.$store.state[stateName]
}
return prev
},{})
}
export const mapGetters = gettersList => {
return gettersList.reduce((prev,gettersName)=>{
prev[gettersName] =function() {return this.$store.getters[gettersName]
}
return prev
},{})
}
export const mapMutations = mutationsList => {
return mutationsList.reduce((prev,mutationsName)=>{
prev[mutationsName] =function(payload){
return this.$store.commit(mutationsName,payload)
}
return prev
},{})
}
export const mapActions = actionsList => {
return actionsList.reduce((prev,actionsName)=>{
prev[actionsName] =function(payload){
return this.$store.dispatch(actionsName,payload)
}
return prev
},{})
}
export default {
install,
Store,
}
Copy the code