Handwriting Vuex core principle

@[toc]

I. Core principles

  1. Vuex is essentially an object
  2. The Vuex object has two properties, the install method and the Store class
  3. The install method is used to mount the store instance to all components. Note that it is the same Store instance.
  4. The Store class has methods such as Commit and Dispatch. The Store class wraps the state passed in by the user into data as an argument to new Vue, thus implementing a response to the state value.

2. Basic preparations

Let’s first build a project using vue-CLI

Delete some unnecessary builds after the project directory is temporarily as follows:

Have put the project on Github: github.com/Sunny-lucki… Can you humble yourself and ask for a star. Do you have any questions or suggestions

We mainly look at the App. Vue, the main js, store/index. Js

The code is as follows:

App.vue

<template>
  <div id="app">
    123
  </div>
</template>
Copy the code

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},mutations: {},actions: {},modules: {}})Copy the code

main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

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

Now let’s start the project. See if the project initialization was successful.

Ok, nothing wrong, initialization succeeded.

Now we’ve decided to create our own Vuex, so create the myvuex.js file

The current directory is as follows

Let’s introduce Vuex and change it to our myVuex

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex' // Modify the code

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},mutations: {},actions: {},modules: {}})Copy the code

Third, analyze the essence of Vuex

Let’s start by asking how Vuex was introduced into the Vue project.

  1. Install Vuex, then passimport Vuex from 'vuex'The introduction of
  2. Var store = new vuex.store ({… New Vue({store});
  3. Vue.use(Vuex) enables each component to have a Store instance

What do we learn from this introduction process?

  1. We get a store instance by new vuex.store ({}), that is, we introduce Vuex with the store class as a property of the Vuex object. Because what you import is essentially a reference to an exported object.

So we can make a preliminary assumption

Class Store{
  
}

let Vuex = {
  Store
}
Copy the code
  1. We also use vue.use (), and one of the principles of vue.use is to execute the install method on the object

So, we can go one step further and assume that Vuex has the install method.

Class Store{
  
}
let install = function(){}let Vuex = {
  Store,
  install
}
Copy the code

At this point, can you write down Vuex roughly?

Simply export the above Vuex object, which is myvuex.js

//myVuex.js
class Store{}let install = function(){}let Vuex = {
    Store,
    install
}

export default Vuex
Copy the code

Let’s execute the project, and if there are no errors, our assumptions are correct.

Oh, my God. It was right. No problem!

Iv. Analyze vue.use

Vue.use(plugin);

(1) Parameters

{ Object | Function } plugin
Copy the code

(2) Usage

Install the vue.js plug-in. If the plug-in is an object, you must provide the install method. If the plug-in is a function, it is treated as the install method. When the install method is called, Vue is passed in as an argument. When the install method is called multiple times by the same plug-in, the plug-in is installed only once.

For more information on how to develop Vue plug-ins, check out this article, which is very simple and takes less than two minutes to read: How to develop Vue plug-ins?

(3) Function

To register the plug-in, simply call the install method and pass Vue as a parameter. But in detail there are two parts of logic to deal with:

1. The type of the plug-in, which can be either the install method or an object containing the install method.

2. The plug-in can be installed only once. Ensure that there are no duplicate plug-ins in the plug-in list.

(4) Implementation

Vue.use = function(plugin){
	const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
	if(installedPlugins.indexOf(plugin)>-1) {return this; } <! -- Other parameters -->const args = toArray(arguments.1);
	args.unshift(this);
	if(typeof plugin.install === 'function'){
		plugin.install.apply(plugin,args);
	}else if(typeof plugin === 'function'){
		plugin.apply(null,plugin,args);
	}
	installedPlugins.push(plugin);
	return this;
}
Copy the code

1. Add the use method to vue.js and accept a parameter plugin.

2. First check whether the plugin has been registered. If it has been registered, the method execution will be terminated directly.

The toArray method is used to convert a class-like array into a real array. Use the toArray method to get arguments. All arguments except the first assign the resulting list to Args, and then add Vue to the top of args’ list. The purpose of this is to ensure that the first parameter of the install method is Vue and the rest of the parameters are passed when the plug-in is registered.

4. Since the plugin parameter supports objects and function types, you can know which way the user uses the plug-in by determining which function is plugin.install or plugin, and then execute the plug-in written by the user and pass args as the parameter.

Finally, add the plugin to installedPlugins to ensure that the same plugin is not registered over and over again. ~~ reminds me of the time when an interviewer asked me why plug-ins wouldn’t be reloaded!! Crying, now I understand.)

Five, improve the install method

We mentioned earlier that vue.use (Vuex) allows each component to have a Store instance.

What does that mean??

To see mian. Js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

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

As you can see, we are only using the Store, the store instance exported by store/index.js, as part of the Vue parameter.

The problem is that Vue is the root component. This means that only the root component has the store value, and no other component has it yet, so we need to make the other components have the store as well.

Therefore, the install method can be perfected this way

Let install = function(Vue){vue.mixin ({beforeCreate(){if (this.$options && this.$options. Store){// if it is the root component This.$store = this.$parent && this.$parent.$store}})}Copy the code

Explain the code:

  1. The Vue parameter is passed as the parameter when we analyze Vue. Use in section 4, and then when we execute install.
  2. Mixins are used to mix the contents of mixins into Vue’s initial parameters, options. Those of you who use Vue have already used mixins.
  3. Why beforeCreate rather than created? Because if you’re doing a created operation,$optionsIt’s already initialized.
  4. If we determine that the current component is the root component, we will hang the store we passed in on the root component instance with the property name$store.
  5. If we determine that the current component is a child component, we will put our root component$storeAlso copy to child components. Pay attention to isReplication of references, so each component has the same$storeMount it on its body.

If the current component is a child, why can we get $store directly from the parent? This reminds me of a question I was once asked by an interviewer: the order in which the parent and child components are executed?

A: Parent beforeCreate-> Parent created -> parent beforeMounte -> child beforeCreate-> child Create-> child beforeMount -> Child Mounted -> parent

BeforeCreate = beforeCreate = beforeCreate = beforeCreate = beforeCreate = beforeCreate = beforeCreate = beforeCreate = beforeCreate

Vi. Implement state of Vuex

    <p>{{this.$store.state.num}}</p>
Copy the code

We all know that we can get the value of state from this statement but we haven’t implemented it in the Store class yet, so obviously, we’re going to get an error right now.

As I said, this is how we use Store

export default new Vuex.Store({
  state: {
    num:0
  },
  mutations: {},actions: {},modules: {}})Copy the code

In other words, let’s take this object

{
  state: {
    num:0
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
}
Copy the code

As a parameter.

So we can get this object directly from the Class Store

class Store{
    constructor(options){
        this.state = options.state || {}
        
    }
}
Copy the code

So can you use it directly?

Try it!

//App.vue
<template>
  <div id="app">
    123
    <p>{{this.$store.state.num}}</p>
  </div>
</template>
Copy the code

Running result:

Isn’t that awesome? How could it be so easy… I can’t believe it.

Oh no, of course it’s not that simple, we’re missing the point that the values in state are also responsive, so we’re not implementing a responsive.

Once an interviewer asked me what the difference is between Vuex and global variables. So that’s the point of noticing the difference

So how do you make it responsive? We know that when we pass in new Vue (), the data we pass in is reactive, so why not pass in a new Vue and pass in state as data? No, that’s it.

class Store{

    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })
    }

}
Copy the code

Now it’s time to implement the response, but how do we get the state? This.$store.vm. State? But it is different from what we usually use, so it needs to be transformed.

We can add a state property to the Store class. This property automatically triggers the GET interface.

class Store{

    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })

    }
    // Add code
    get state() {return this.vm.state
    }


}
Copy the code

This is ES6 syntax, kind of like the get interface of object.defineProperty

Successful implementation.

Implement getters

//myVuex.js
class Store{

    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })
        // Add code
        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName= >{
            Object.defineProperty(this.getters,getterName,{
                get:() = >{
                    return getters[getterName](this.state)
                }
            })
        })

    }
    get state() {return this.vm.state
    }
}

Copy the code

We save the getter that the user passed in to our getters array.

The most interesting thing is that there are often interview questions asking why you don’t use parentheses when you use getters. If I hadn’t learned the handwriting Vuex, I wouldn’t have been puzzled. So this question is like asking why we don’t use parentheses when we write a variable. (e.g. {{num}} instead of {{num()}}))

Object. DefineProperty get interface

Ok, so let’s see if we can use the getter.

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  // Add test code
  getter: {getNum:(state) = >{
      return state.num
    }
  },
  mutations: {},actions: {},})Copy the code
<template>
  <div id="app">
    123
    <p>State: {{this. $store. State. The num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
  </div>
</template>
Copy the code

Perfect. No accidents.

Viii. Implement mutation

//myVuex.js
class Store{

    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName= >{
            Object.defineProperty(this.getters,getterName,{
                get:() = >{
                    return getters[getterName](this.state)
                }
            })
        })
        // Add code
        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName= >{
            this.mutations[mutationName] = (arg) = > {
                mutations[mutationName](this.state,arg)
            }
        })

    }
    get state() {return this.vm.state
    }
}
Copy the code

Mutations are the same as getters, they’re storing the mutations that come in from the user using the mutations object.

But how? Recall how we triggered mutations.

this.$store.commit('incre',1)
Copy the code

Yes, in this form. You can see that the store object has the commit method. The COMMIT method triggers some corresponding method on the mutations object, so we can add the COMMIT method to the Store class

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName= >{
            Object.defineProperty(this.getters,getterName,{
                get:() = >{
                    return getters[getterName](this.state)
                }
            })
        })
        
        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName= >{
            this.mutations[mutationName] =  (arg) = > {
                mutations[mutationName](this.state,arg)
            }
        })

    }
    // Add code
    commit(method,arg){
        this.mutations[method](arg)
    }
    get state() {return this.vm.state
    }
}
Copy the code

All right, so let’s test that out.

<template>
  <div id="app">
    123
    <p>state:{{this.$store.state.num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
    <button @click="add">+ 1</button>
  </div>
</template>
// Add test code
<script>
  export default {
      methods: {add(){
              this.$store.commit('incre'.1)}}}</script>
Copy the code

store/index.js

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  getter: {getNum:(state) = >{
      return state.num
    }
  },
  // Add test code
  mutations: {
    incre(state,arg){
        state.num += arg
    }
  },
  actions: {},})Copy the code

The operation succeeds.

Implement actions

When they achieve mutations, the implementation of actions is very simple, very similar, if you look at the code:

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName= >{
            Object.defineProperty(this.getters,getterName,{
                get:() = >{
                    return getters[getterName](this.state)
                }
            })
        })

        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName= >{
            this.mutations[mutationName] =  (arg) = > {
                mutations[mutationName](this.state,arg)
            }
        })
        // Add code
        let actions = options.actions
        this.actions = {}
        Object.keys(actions).forEach(actionName= >{
            this.actions[actionName] = (arg) = >{
                actions[actionName](this,arg)
            }
        })

    }
    // Add code
    dispatch(method,arg){
        this.actions[method](arg)
    }
    commit(method,arg){
        console.log(this);
        this.mutations[method](arg)
    }
    get state() {return this.vm.state
    }
}
Copy the code

It’s the same, but there’s one thing that needs to be explained, which is why I’m passing this in here. This represents the Store instance itselfThis is because we use actions like this:

  actions: {
    asyncIncre({commit},arg){
        setTimeout(() = >{
          commit('incre',arg)
        },1000)}},Copy the code

{commit} is the deconstruction of this, the store instance

So let’s test that out.


<template>
  <div id="app">
    123
    <p>state:{{this.$store.state.num}}</p>
    <p>getter:{{this.$store.getters.getNum}}</p>
    <button @click="add">+ 1</button>
    <button @click="asyncAdd">Asynchronous + 2</button>
  </div>
</template>

<script>
  export default {
      methods: {add(){
              this.$store.commit('incre'.1)},asyncAdd(){
              this.$store.dispatch('asyncIncre'.2)}}}</script>
Copy the code

store/index.js

//store/index.js
import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  getter: {getNum:(state) = >{
      return state.num
    }
  },
  mutations: {
    incre(state,arg){
        state.num += arg
    }
  },
  // Add test code
  actions: {
    asyncIncre({commit},arg){
        setTimeout(() = >{
          commit('incre',arg)
        },1000)}}})Copy the code

Oh my God, there’s an error, and what it says here is that when I get here I find that this is undefined.

However, no, we also implemented here when the mutation was implemented, and the implementation succeeded ah.

Let’s break it down:

this.$store.commit('incre'.1)
Copy the code

So when we execute this code, when we commit, we’re referring to whoever called this, so this points to $store.

this.$store.dispatch('asyncIncre'.2)
Copy the code

Execute this code, and it will execute

asyncIncre({commit},arg){
    setTimeout(() = >{
      commit('incre',arg)
    },1000)}Copy the code

Do you see the problem? Who calls COMMIT?? Is $store? And it isn’t. So to solve this problem, we have to replace it with the arrow function

//myVuex.js
class Store{
    constructor(options) {
        this.vm = new Vue({
            data: {state:options.state
            }
        })

        let getters = options.getter || {}
        this.getters = {}
        Object.keys(getters).forEach(getterName= >{
            Object.defineProperty(this.getters,getterName,{
                get:() = >{
                    return getters[getterName](this.state)
                }
            })
        })

        let mutations = options.mutations || {}
        this.mutations = {}
        Object.keys(mutations).forEach(mutationName= >{
            this.mutations[mutationName] =  (arg) = > {
                mutations[mutationName](this.state,arg)
            }
        })

        let actions = options.actions
        this.actions = {}
        Object.keys(actions).forEach(actionName= >{
            this.actions[actionName] = (arg) = >{
                actions[actionName](this,arg)
            }
        })

    }
    dispatch(method,arg){
        this.actions[method](arg)
    }
    // Modify the code
    commit=(method,arg) = >{
        console.log(method);
        console.log(this.mutations);
        this.mutations[method](arg)
    }
    get state() {return this.vm.state
    }
}
Copy the code

To test again

Perfect end !!!!

$store.state.xx = “” $store.state.xx = “” Is that ok? There’s no problem with this assignment, and state is still reactive. So why reinvent the wheel with commit?

Vuex can record every state change record, save state snapshot, implement time roaming/rollback and other operations.

One interesting thing that occurred to me is that if we were to implement the simplest version of Vuex, we would just implement state. No other getters, no action, no commit. It feels like I’m packing light. It can be done.

When implemented, it turns out that state is similar to a global variable, except that state is responsive.

Do you have any questions or suggestions

Thank you also congratulations you see here, I can humble beg a star!!

Github:github.com/Sunny-lucki…