In-depth Vuex best practices

preface

Before starting the body of the bs a first, I wrote an article on the interview last week had fire πŸ€“, happy the whole morning, in fact, I also just started to write articles must be less than the quality of those bosses, so can I also feel very accidental fire, very grateful to the support of my friends, I also will try to spend time in this output better articles for the reader, A lot of friends ask me how TO learn by myself. You can look at my blog. I have written about my self-study method.

start

The core concept

Okay, now let’s get back to the text. Before we talk about Vuex, what is Vuex?

Vuex is a state management mode developed specifically for vue.js applications. It uses centralized storage to manage the state of all components of an application and rules to ensure that the state changes in a predictable way

It is a set of centralized state management mode library developed specifically for vue.js.

I can share data globally by defining Store objects globally. What are the benefits of using Vuex to manage data? What else does it solve?

  • Centralized management of shared data in Vuex for easy development and maintenance (as reflected in Vuex single data flow and predictable data changes)
  • It can efficiently realize data sharing and integration between componentsdevtools extensionConvenient debugging, improve development efficiency
  • The data stored in Vuex is responsive, and when the data changes, the data in the view is updated synchronously (this is the core point of Vuex).

So based on the above three advantages, we can understand, Shared data based on Vuex centralized management, solve the problem of data sharing between multiple components, and because the data is the response type, so the data change view will be updated, so we use Vuex then I don’t need to pay attention to different views (components) rely on the same state (data), We can focus all our energy on status updates and Vuex will take care of that for us.

After explaining the concept of Vuex, let’s take a look at the following two official pictures

  • state, the data source that drives the application;
  • viewIn a declarative mannerstateMap to view;
  • actions, the response inviewState changes caused by user input on

This is a simple diagram of a single data flow, and the problem with it is:

  • Multiple views depend on the same state.
  • Actions from different views need to change the same state.

So there is Vuex centralized state management mode

Core features of Vuex

The picture above gives a good explanation of the characteristics of Vuex. Before explaining what the picture wants to express, let’s explain what roles the words in the picture below represent.

  • State

    State provides the only common data source, and all shared data is stored in State in the Store

  • Mutation

    Mutation is used to modify data in the $store

  • Action

    Mutations cannot write asynchronous code, which will cause the vUE debugger to display errors. In VUEX we can use actions to perform asynchronous operations.

  • Getter

    A Getter is used to process the data in a Store to form new data. It only wraps the data stored in the Store and does not modify the data stored in the Store. When the data in the Store changes, the content generated by the Getter also changes

Manual practice

It doesn’t matter if you don’t understand it now, let’s experience the complete process of the above diagram through practice, and explain the meaning of this diagram later.

Tip: This article is about practice, so the amount of code will be a little bit too much.

Vue create vuex(the name of your project) after creating the project, there is a store folder and index is your state management library. Next, let's experience the complete vuex state management process.Copy the code

Modified index. Js

 import Vue from 'vue'
 import Vuex from 'vuex'
 import state from './state'
 import getters from './getters'
 import actions from './actions'
 import mutations from './mutations'
 
 Vue.use(Vuex)
 // Export an instance of Store
 export default new Vuex.Store({ ********
   state, / / the data source
   getters, // The data source can be processed twice
   actions, // It is used to trigger the function in mutations to modify the data in state, which is mainly used to remedy the problem that mutations cannot write asynchronous code
   mutations // Used to modify data in the data source
 })
Copy the code

Create the state.js file

 const defaultLevel = 'Primary Front-end Development'
 const salary = '5000'
 const ages = [3.2.1.4.52.20.22.10];
 
 
 export default { // Three data sources are provided
   defaultLevel,
   salary,
   ages
 }
Copy the code

Create mutations. Js

 const upgrade = (state, newLevel) = > {
   state.defaultLevel = newLevel
 }
 
 const upSalary = (state, newSalary) = > {
   state.salary = newSalary
 }
 
 export default { // Provides two methods to modify the data source
   upgrade,
   upSalary
 }
Copy the code

Create actions. Js

 export default {
   changeLevel({commit}, newLevel) { // Asynchronous code can be written in actions
       return new Promise((resolve) = > {
         setTimeout(() = > {
          commit('upgrade', newLevel)
          console.log('Fight monsters upgrade... ');
          console.log('Fight monsters upgrade... ');
          console.log('Fight monsters upgrade... ');
          resolve('Upgrade completed')},2000); }})},Copy the code

Create getters. Js

 /* eslint-disable */
 const filterAge = (state) = > (term) = > state.ages.filter((age) = > age > term) / / ES6 syntax
 
 export default { // provides a way to filter data sources
   filterAge
 }
 
Copy the code

The final overall table of contents

app.js

<template> <div class="app"> <div class="example1"> <h1> {{salary}}</div> < button@click ="upgradeHandler"> Upgrade </button> </div> <div class="example2"> < h1 > example 2 < / h1 > < ul > < li v - for = "item in which" : the key = "item. The toString ()" > {{item}} < / li > < / ul > < button </span> <ul> <li v-for="item in newAge" :key='item.toString()'>{{item}}</li> </ul> </div> </div> </div> </template> <script> import { mapState, mapGetters, mapActions, mapMutation, mapMutations, } from 'vuex' export default { name: 'app', data() { return { newAge: []}}, computed: {// Export two data in state using an auxiliary function in computed properties... mapState(['defaultLevel', 'salary', 'ages']), }, methods: { ... mapActions(['changeLevel', 'changeLevel2']), ... mapMutations(['upSalary']), Async upgradeHandler() {let ret = await this.changelevel (' intermediate front-end development ') // Result of waiting for asynchronous execution console.log(ret); SetTimeout (() => {this.upsalary (10000) // Sync code without actions}, 1500); }, filterHandler() { this.newAge = this.$store.getters.filterAge(10) } } } </script> <style> .example1 { margin-bottom: 50px; padding-bottom: 30px; border-bottom: 2px solid #000; } </style>Copy the code

Finally, let’s look at the effects of the two examples above

Example 1

Example 2

It is recommended to write down the code first and then see the effect

Having written the above code, I believe that you have already experienced the benefits of Vuex, so let me explain Vuex in plain English

Vuex solves the above problem. The component references state to the data source to present the view. I use manual dispatch to trigger the method in Actions commit and trigger mutations to change state to rerender the data and change the component.

Vuex is a single data stream to manage data, and can change data in a predictable and single way.

I want to modify the data in state:

  • Dispatch to trigger the methods in the Action
  • Commit in actions to trigger mutations
  • Finally, mutations will modify the data in state

It is not difficult to see that I have performed so many operations in order to change the data in state, but in the end, the only way that can directly modify the data in state is commit to trigger mutations. Those in the middle are great for writing middleware, like vuex-persis.

Okay, now let’s write a todoList case to solidify it.

Easy to see the effect of the code, you can click here to see the effect of the implementation, source repository:

example

A. Initialization items

You can choose to re-initialize a VuEX project, or you can use the current one, so let’s use this one.

Then open the public create API folder and create a list.json file to simulate the data:

[{"id": 0."info": "Racing car sprays burning fuel into crowd."."done": false
    },
    {
        "id": 1."info": "Japanese princess to wed commoner."."done": false
    },
    {
        "id": 2."info": "Australian walks 100km after outback crash."."done": false
    },
    {
        "id": 3."info": "Man charged over missing wedding girl."."done": false
    },
    {
        "id": 4."info": "Los Angeles battles huge wildfires."."done": false}]Copy the code

Next, install the libraries and plug-ins needed for the project

$ npm install vue-router axios ant-design-vue babel-plugin-import less-loader node-less --save-dev
Copy the code

Note: An error will be reported if the less version uses Ant-design-vue above 3.x. My version is 3.10.3. There are many people on the issue to solve this problem, and solutions may be different for different version environments.

Issue address

My solution: create vue.config.js to add the following code

module.exports = {
  css: {
      loaderOptions: {
          less: {
            lessOptions: {javascriptEnabled: true,}}}},}Copy the code

Next, open main.js and add store index.js’ ‘as follows:

import Vue from 'vue'
import App from './Doto.vue'
import store from '/ store under ` index. Js `'


/* Complete import mode **/
// 1. Import the ant-design-vue component library
import Antd from 'ant-design-vue'
// 2. Import the component library stylesheet
import 'ant-design-vue/dist/antd.css'
// 3. Install the component library
Vue.use(Antd)

/** Load on demand: import modules, no need to import styles **/
import { List, Button, Input, Checkbox} from 'ant-design-vue'
// Use the component
Vue.use(List)
Vue.use(Button)
Vue.use(Input)
Vue.use(Checkbox)

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

Then open the store folder in index.js and add the following code to the AXIos json file:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // All task lists
    list: [].// The value in the text input box
    inputValue: 'Beige'
  },
  mutations: {
    initList(state, list) {
      state.list = list
    },
    setInputValue(state,value){
      state.inputValue = value
    }
  },
  actions: {
    getList(context) {
      axios.get('api/list.json').then(({ data }) = > {
        console.log(data);
        context.commit('initList', data)
      })
    }
  }
})
Copy the code

Finally, create doto. vue and configure the route to fetch and display the data in store:

<template> <div class="doto"> < A-input placeholder=" my_ipt :value="inputValue" @change="handleInputChange" /> <a-button type="primary"> < A-list bordered :dataSource="list class="dt_list"> <a-list-item slot="renderItem" slot-scope="item"> <! - check box - > < : a - the checkbox checked = "item. Done" > {{item. The info}} < / a - the checkbox > <! - delete link -- -- > < a slot = "actions" > delete < / a > < / a - list - item > <! <div slot="footer" class="footer"> <! -- Number of unfinished tasks --> <span>0 remaining </span> <! <a-button> < A-button type="primary"> All </a-button> < A-button > <a-button> Completed </a-button> </a-button-group> <! </a> </div> </a-list> </div> </template> <script> import {mapState} from 'vuex' export default { name: 'app', data() { return { // list:[] } }, created(){ // console.log(this.$store); this.$store.dispatch('getList') }, methods:{ handleInputChange(e){ // console.log(e.target.value) this.$store.commit('setInputValue',e.target.value) } }, computed:{ ... mapState(['list','inputValue']) } } </script> <style scoped> .doto { margin: 20px 50px; } .my_ipt { width: 500px; margin-right: 10px; } .dt_list { width: 500px; margin-top: 10px; } .footer { display: flex; justify-content: space-between; align-items: center; } </style>Copy the code

B. Complete the added items

First, open the doto. vue file and bind the click event to the “Add Event” button. You can also add keyboard events to the form and write handlers

// Bind events
<a-button type="primary" @click="addItemToList"</a-button><a-input placeholder="Please enter the task" class="my_ipt" :value="inputValue" @change="handleInputChange"  @keydown.enter="addItemToList"/>

// Write an event handler
methods:{
    ......
    addItemToList(){
      // Add items to the list
      if(this.inputValue.trim().length <= 0) {return this.$message.warning('Text box contents cannot be empty')}this.$store.commit('addItem')}}` `'js' and open' store 'index.js` write `addItem` `` `js
export default new Vuex.Store({
  state: {
    // All task lists
    list: [].// The value in the text input box
    inputValue: 'AAA'.// Next id
    nextId:5
  },
  mutations: {...// Add a list item
    addItem(state){
      const obj = {
        id :state.nextId,
        info: state.inputValue.trim(),
        done:false
      }
      // Add the created items to the array list
      state.list.push(obj)
      // Increment the nextId value
      state.nextId++
      state.inputValue = ' '}}... })Copy the code

C. Complete the deletion

First, open the doto. vue file, bind the click event to the “Delete” button, and write a handler

// Bind events
<a slot="actions" @click="removeItemById(item.id)"> delete < / a >// Write an event handler
methods:{
    ......
    removeItemById(id){
      // Delete items by id
      this.$store.commit('removeItem',id)
    }
  }
Copy the code

And then I’m going to open index under Store and write removeItem

export default new Vuex.Store({
  ......
  mutations: {...removeItem(state,id){
      // Delete item data by id
      const index = state.list.findIndex( x= > x.id === id )
      // console.log(index);
      if(index ! = -1) state.list.splice(index,1); }}... })Copy the code

D. The selected status is changed

First, open the doto. vue file, bind the click event to the “check” button, and write a handler

// Bind events
<a-checkbox :checked="item.done" @change="cbStateChanged(item.id,$event)">{{item.info}}</a-checkbox>

// Write an event handler
methods:{
    ......
    cbStateChanged(id,e){
      // Triggered when the check box status changes
      const param = {
        id:id,
        status:e.target.checked
      }

      // Change the event status according to the ID
      this.$store.commit('changeStatus',param)
    }
  }
Copy the code

Then open index.js in store and write changeStatus

export default new Vuex.Store({
  ......
  mutations: {...changeStatus(state,param){
      // Change the state of the corresponding item according to the ID
      const index = state.list.findIndex( x= > x.id === param.id )
      if(index ! = -1) state.list[index].done = param.status
    }
  }
  ......
})
Copy the code

E. Statistics of remaining items

Open index.js under store and add getters to complete the statistics of the remaining items

getters:{
  unDoneLength(state){
    const temp = state.list.filter( x= > x.done === false )
    console.log(temp)
    return temp.length
  }
}
Copy the code

Open doto. vue and use getters to display the remaining items

// Display the remaining items using the calculated attributes mapped<! -- Number of unfinished tasks --><span>{{unDoneLength}} a surplus</span>

/ / import getters
import { mapState,mapGetters } from 'vuex'
/ / map
computed: {... mapState(['list'.'inputValue']),
  ...mapGetters(['unDoneLength'])}Copy the code

F. Clear completed items

First, open the doto. vue file, bind the click event to the “Clean Done” button, and write a handler

<! -- Clear the list of completed tasks --><a @click="clean">Cleanup complete</a>

// Write an event handler
methods:{
  ......
  cleanDone(){
    // Clear the completed items
    this.$store.commit('cleanDone')}}Copy the code

Then open index.js in store to write cleanDone

export default new Vuex.Store({
  ......
  mutations: {...cleanDone(state){
      state.list = state.list.filter( x= > x.done === false)}}... })Copy the code

G. Click the TAB to switch events

Open doto. vue, bind click events to the “All”, “Incomplete”, and “Done” tabs, write a handler and change the list data source to a getters.

<a-list bordered :dataSource="infoList" class="dt_list">... <! -- Operation button --><a-button-group>
    <a-button :type="viewKey ==='all'? 'primary':'default'" @click="changeList('all')">all</a-button>
    <a-button :type="viewKey ==='undone'? 'primary':'default'" @click="changeList('undone')">unfinished</a-button>
    <a-button :type="viewKey ==='done'? 'primary':'default'" @click="changeList('done')">Has been completed</a-button>
  </a-button-group>. </a-list>// Write event handlers and map calculated properties
methods:{
  ......
  changeList( key ){
    // Click "All", "Done", "not done" to trigger
    this.$store.commit('changeKey',key)
  }
},
computed: {... mapState(['list'.'inputValue'.'viewKey']),
  ...mapGetters(['unDoneLength'.'infoList'])}Copy the code

Open the index.js file in store and add getters, mutations, state

export default new Vuex.Store({
  state: {...// Save the default TAB values
    viewKey:'all'
  },
  mutations: {...changeKey(state,key){
      // Triggered when the user clicks the "All", "Done", and "unfinished" tabs
      state.viewKey = key
    }
  },
  ......
  getters: {...infoList(state){
      if(state.viewKey === 'all') {return state.list
      }
      if(state.viewKey === 'undone') {return state.list.filter( x= > x.done === false)}if(state.viewKey === 'done') {return state.list.filter( x= > x.done === true)}}}})Copy the code

Vuex principle analysis

Ok, after the above cases, I believe you have almost understood the use of Vuex. Next, let’s talk about the principle of Vuex. What is the principle of Vuex?

The key principle of Vuex is to use Vue instances to manage state (data) and achieve data responsiveness

Let’s take a look at the effects:

Next, we will analyze the internal principles of Vuex in the form of code

<html>
  <head>
    <title>Vuex principle analysis</title>
    <script src='./vue.js'></script>
  </head>
  <body>
   <! We define three vue instances at the DOM level.
    <div id="root">{{data}}</div>
    <div id="root2">{{data2}}</div>
    <div id="root3">
      <button @click="change">change</button>
    </div>
      
    <script>
     // Define a Vuex plug-in
     function registerPlugin(Vue) {... }// Use this plugin
     Vue.use(registerPlugin)
     new Vue({
        el: '#root'.computed: { // Change referenced views in real time based on data changes by calculating properties
          data() {
            return this.$store.state.message
          }
        }
      })
      new Vue({
        el: '#root2'.computed: {
          data2() {
            return this.$store.state.message
          }
        }
      })
      new Vue({
        el: '#root3'.methods: {
          change() { // Provide a change method to change the state in the store
            const newValue = this.$store.state.message + '. '
            this.$store.mutations.setMessage(newValue)
          }
        }
      })
    </script>
  </body>
Copy the code

Imitate Vuex source code implementation

First, Vuex is mounted as a plug-in. Then, step by step, the Vuex plug-in is implemented

  • To achieve the Store class
  • Maintain a reactive state
  • Implement the commit ()
  • Realize the dispatch ()
  • Implement dgetters
  • Mount $store

Modify the mian. Js

// Use your own Vuex plugin
import store from './my-store' 

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

Add my store/index. Js

import Vue from 'vue'
import Vuex from './my-vuex' // Introduce our own Vuex plugin

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 1
  },
  mutations: {
    // Where does state come from
    add(state) {
      state.counter++
      console.log(this); }},actions: {
    // commit -> means we need to pass in the store instance
    add({commit}) {
      setTimeout(() = > {
        commit('add')},1000); }},getters: {
    doubleCounter: state= > {
      return state.counter * 2; }},modules: { // Namespaces are not implemented for the time being}})Copy the code

Using a plug-in in Vue we need through the Vue. Use way to use, and if the plug-in: provides the install method of object | a function.

Let’s implement the Store class (plug-in)

Add my store/index. Js

class Store {
  constructor(options) {
    // Initialize the Store instance with the passed options
	}
  
   commit(type, payload) {
     // type: muations method name,
     // Payload: Data to be transmitted
   }
  
  dispatch(type, payload) {
    // type: actions method name,
    // Payload: Data to be transmitted}}function install(_Vue, options, ....) {
	/ / _Vue object
  // options -> vue. use(Vuex, can pass plug-in configuration items)
}

export default { Store, install }
Copy the code

install

Next, implement the install method

my-store/index.js

let Vue // Share the Vue object globally
function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      // This refers to the component instance
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store; }}})}Copy the code

We can attach the store object of the root component instance to the prototype of the Vue by providing the beforeCreate hook to the global mixin for Vue objects passed in using Vuex. Ensure that all subsequent instances instantiated by Vue objects can find the store object provided by the initializing root component instance through the prototype chain lookup mechanism

This is the paragraph in mian.js

new Vue({
  router,
  store,
  render: h= > h(App)
}).$mount('#app')
// The configuration items provided by the initialization Vue can be obtained through the instance object this.$options
// As long as there is store at initialization, you are considered to be the component root instance
Copy the code

The install method is complete

The Store is initialized

Next we implement Store initialization

  • Save option
  • Respond to state

my-store/index.js

class Store {
  constructor(options) {
    let {
    	mutations
      actions
      getters
      state
      / /... The other options
    } = options 
    // Save options
    this._mutations = mutations
    this._actions = actions
    
    // respond to state
    this._vm = new Vue({
      data() {
        return {
          // $$does not proxy -> So that data defined in data is not placed on the outermost layer of the instance (to prevent users from tamper with the state object)
          $$state: state,
        }
      },
    })
    
  }
  
  get state() {
    return this._vm._data.$$state
  }
  
  set state(v) {
    throw Error('Please reset state using repalceState')}}Copy the code

The key principle of Vuex is to use Vue instances to manage state (data) and to achieve data responsiveness

This._vm = new Vue() passes the state object as data, and Vue automatically implements data responsiveness. After this

To see if the data response is successful

my-store/index.js

class Store {
  constructor(options) {
+ setInterval(() => {
+ this.state.counter++
+}, 1000);
+}
}
Copy the code

App.vue

<div id="app"> <! -... --> <p>{{ $store.state.counter }}</p> </div>Copy the code

Rendering πŸ’—

commit/dispatch

Then implement the COMMIT/Dispatch method to provide the user with an API that can call the mutations method to manipulate state data

my-store/index.js

class Store {
  commit(type, payload) {
    // Get that function from mutations configured by the user according to type
    const entry = this._mutations[type]
    if(! entry) {console.error('unknown mutation. ');
      return 
    }
    // Commit in the context of the Store instance
    entry.call(this.this.state, payload)
  }
  
  dispatch(type, payload) {
    const entry = this._actions[type]
    if(! entry) {console.error('unknown action! ');
      return 
    }
    // The context of dispatch is the Store instance
    entry.call(this.this, payload)
  }
}
Copy the code

app.vue

<template> <div id="app"> <! -... --> <p @click="$store.commit('add')"> commit: {{$store.state.counter}}</p> <p @click="$store.dispatch('add')">dispatch: {{$store.state.counter}}</p> </div> </template>Copy the code

Rendering πŸ’—

The commit/dispatch implementation can be derived step by step via $store.com MIT /dispatch(‘add’). The Vuex principle is very simple

getters

my-store/index.js

Getters provides methods to each member, and we pass state data so that the user can do secondary processing on the state data.

function setGetters(state, getters) {
  for (let key in getters) {
    return Object.defineProperty({}, key, {
      get () {
        let fn = Reflect.get(getters, key)
        if (typeof fn === 'function') {
          return fn(state)
        }
        return undefined
      },
      set(key) {
        console.error(`Cannot set ${key}`)}})}}Copy the code
class Store {
  constructor(options) {
+ this.getters = setGetters(
+ this._vm._data.$$state,
+ options.getters
+)}}Copy the code

App.vue

+ <div>getters: {{$store.getters.doubleCounter}}</div>
Copy the code

Source address, welcome Strat🌟

Write in the last

Since this is a practical article, the entire article is written in code, with less conceptual and basic syntax. Please like πŸ€“ if this article was helpful to you

Practice makes perfect, shortage in play

If there is that piece of writing is not good or there is a problem, you are welcome to point out, I will also keep modifying in the following article. I hope I can grow with you as I progress. Like my article friends can also pay attention to it

I will be grateful for the first attention. At this time, young you and I, travel light; Then, rich you and I, full of goods.