"Vue source code learning" do you want to know the implementation principle of Vuex?

Posted by Skoalbasher on Sat, 25 Dec 2021 06:05:18 +0100

Hello, I'm Lin Sanxin. Vuex is a special for Vue JS application development state management mode. It uses centralized storage to manage the state of all components of the application, and uses corresponding rules to ensure that the state changes in a predictable way.

Under what circumstances should I use Vuex?

Vuex can help us manage shared state with more concepts and frameworks. This requires a trade-off between short-term and long-term benefits.

If you don't plan to develop large single page applications, using Vuex can be cumbersome and redundant. That's true - if your application is simple enough, you'd better not use Vuex. A simple store mode (opens new window) is enough for you. However, if you need to build a medium and large-scale single page application, you are likely to consider how to better manage the state outside the component, and Vuex will become a natural choice. Dan Abramov, the author of Redux, is quoted as saying:

Flux architecture is like glasses: you know when you need it.

Review the use of Vuex

install

Yarn

yarn add vuex

NPM

npm install vuex --save

In a modular packaging system, you must explicitly use Vue Use() to install Vuex:

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

Vue.use(Vuex)

Register store

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

new Vue({
  el: '#app',
  store // register
})

State

  1. Common use

    const Counter = {
      template: `<div>{{ count }}</div>`,
      computed: {
     count () {
       return this.$store.state.count
     }
      }
    }

    Whenever this$ store. state. When the count changes, the calculation attribute will be re evaluated and the associated DOM will be triggered to be updated.

  2. auxiliary function

    When a component needs to obtain multiple states, declaring these states as calculation attributes will be somewhat repetitive and redundant. To solve this problem, we can use the mapState auxiliary function to help us generate calculation properties, so that you can press the key a few times less:

    // In the separately built version, the helper function is vuex mapState
    import { mapState } from 'vuex'
    
    export default {
      // ...
      computed: mapState({
     // Arrow functions make the code simpler
     count: state => state.count,
    
     // The string parameter 'count' is equivalent to ` state = > state count`
     countAlias: 'count',
    
     // In order to use 'this' to get the local state, you must use the regular function
     countPlusLocalState (state) {
       return state.count + this.localCount
     }
      })
    }

    When the name of the mapped calculated attribute is the same as the child node name of state, we can also pass a string array to mapState.

    computed: mapState([
      // Map this Count is store state. count
      'count'
    ])

    Object expansion operator

    computed: {
      localComputed () { /* ... */ },
      // Use the object expansion operator to blend this object into an external object
      ...mapState({
     // ...
      })
    }

Getters

  1. Common use

    Getter accepts state as its first parameter:

    const store = new Vuex.Store({
      state: {
     todos: [
       { id: 1, text: '...', done: true },
       { id: 2, text: '...', done: false }
     ]
      },
      getters: {
     doneTodos: state => {
       return state.todos.filter(todo => todo.done)
     }
      }
    })

    getter can also accept other getters as the second parameter:

    getters: {
      // ...
      doneTodosCount: (state, getters) => {
     return getters.doneTodos.length
      }
    }

    We can easily use it in any component:

    computed: {
      doneTodosCount () {
     return this.$store.getters.doneTodosCount
      }
    }

    Note that getter s are cached as part of Vue's responsive system when accessed through properties. (the same is true for the computed cache. I will write an article about it later.)

You can also pass parameters to getters by letting getters return a function. It is very useful when you query the array in the store.

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
  1. auxiliary function

    The mapGetters auxiliary function only maps getters in the store to local calculation properties:

    import { mapGetters } from 'vuex'
    
    export default {
      // ...
      computed: {
      // Use the object expansion operator to mix getter s into the computed object
     ...mapGetters([
       'doneTodosCount',
       'anotherGetter',
       // ...
     ])
      }
    }

    If you want to give a getter property another name, use the object form:

    ...mapGetters({
      // Put ` this Donecount ` is mapped to ` this$ store. getters. doneTodosCount`
      doneCount: 'doneTodosCount'
    })

    Muations

  2. Common use

    The mutation in Vuex is very similar to events: each mutation has a string event type and a callback function (handler)

    const store = new Vuex.Store({
      state: {
     count: 1
      },
      mutations: {
     increment (state, n) { // n is a parameter, which can be set or not. This parameter is also called "load"
       // Change status
       state.count++
     }
      }
    })
    // use
    this.$store.commit('increment', 10)
  3. auxiliary function

    import { mapMutations } from 'vuex'
    
    export default {
      // ...
      methods: {
     ...mapMutations([
       'increment', // Add ` this Increment() ` is mapped to ` this$ store. commit('increment')`
    
       // `mapMutations ` also supports loads:
       'incrementBy' // Add ` this Incrementby (amount) ` map to ` this$ store. commit('incrementBy', amount)`
     ]),
     ...mapMutations({
       add: 'increment' // Add ` this Add() ` is mapped to ` this$ store. commit('increment')`
     })
      }
    }

    Mixing asynchronous calls in mutation will make your program difficult to debug. For example, when you call two mutation with asynchronous callbacks to change the state, how do you know when to call back and which to call back first? That's why we have to distinguish between these two concepts. In Vuex, mutation is a synchronous transaction

    Action

    Action is similar to mutation in that:

    • The Action submits the mutation instead of directly changing the status.
    • An Action can contain any asynchronous Action.

    const store = new Vuex.Store({
      state: {
     count: 0
      },
      mutations: {
     increment (state) {
       state.count++
     }
      },
      actions: {
       // The Action function accepts a context object with the same methods and properties as the store instance
      incrementAsync (context , n) { // Transmissible "load" n
        setTimeout(() => {
          context.commit('increment') 
        }, 1000)
       }
      }
    })
    // implement
    // Distribute as load
    store.dispatch('incrementAsync', {
      amount: 10
    })
    
    // Distribute as objects
    store.dispatch({
      type: 'incrementAsync',
      amount: 10
    })
  4. auxiliary function

    import { mapActions } from 'vuex'
    
    export default {
      // ...
      methods: {
     ...mapActions([
       'increment', // Add ` this Increment() ` is mapped to ` this$ store. dispatch('increment')`
    
       // `mapActions ` also supports loads:
       'incrementBy' // Add ` this Incrementby (amount) ` map to ` this$ store. dispatch('incrementBy', amount)`
     ]),
     ...mapActions({
       add: 'increment' // Add ` this Add() ` is mapped to ` this$ store. dispatch('increment')`
     })
      }
    }
  5. Combined Action

    // Suppose getData() and getOtherData() return Promise
    actions: {
      async actionA ({ commit }) {
     commit('gotData', await getData())
      },
      async actionB ({ dispatch, commit }) {
     await dispatch('actionA') // Wait for actionA to complete
     commit('gotOtherData', await getOtherData())
      }
    }

Module

Due to the use of a single state tree, all the states of the application will be concentrated in a relatively large object. When the application becomes very complex, the store object may become quite bloated.

In order to solve the above problems, Vuex allows us to split the store into modules. Each module has its own state, mutation, action, getter, and even nested sub modules - split in the same way from top to bottom:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // ->Status of ModuleA
store.state.b // ->Status of moduleb

For the mutation and getter inside the module, the first parameter received is the local state object of the module.

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // The 'state' object here is the local state of the module
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      // The 'state' object here is the local state of the module
      return state.count * 2
    }
  }
}

Similarly, for action s inside the module, the local state is through context State is exposed, and the root node state is context rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

For getter s inside the module, the root node status will be exposed as the third parameter:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

The namespace part of the module. I'll talk about it later

Simple principle implementation

explain

After reading the Vuex source code file, I found that there are indeed many. Let me talk about the source code of some of our most commonly used functions

In fact, students who have used Vuex know that we use this in our pages or components$ store. XXX. In fact, we just need to assign the store object you created to the $store variable in the page or component

Generally speaking, the principle of Vuex is to use global mixing Mixin to mix the store object you created into each Vue instance. What is global mixing? for instance:

import Vue from 'vue'
// Global blending
Vue.mixin({
  created () {
      console.log('I'm Lin Sanxin')
  }
})

// All Vue instances created later will output 'I'm Lin Sanxin'
const a = new Vue({
  // There's nothing here, but it can output 'I'm Lin Sanxin'
})
// =>"I'm Lin Sanxin"
const b = new Vue({
  // There's nothing here, but it can output 'I'm Lin Sanxin'
})
// =>"I'm Lin Sanxin"

Those who understand the above example will know. Similarly, put the console Log ('I'm Lin Sanxin ') is replaced by a code that can do this: assign store to the $store attribute of the instance to achieve:

code implementation

catalogue

  1. vuex.js

    // vuex.js
    let Vue;
    
    // The install method is set because Vue Use (xxx) will execute the install method of XXX
    const install = (v) => { // Parameter v is responsible for receiving vue instances
     Vue = v;
     // Global blending
     Vue.mixin({
         beforeCreate() {
             if (this.$options && this.$options.store) {
                 // On the root page, you can directly assign your store to your $store,
                 This also explains why it is used vuex You have to put store Put in the entry file main.js Root in Vue In the example
                 this.$store = this.$options.store;
             } else {
                 // In addition to the root page, assign the superior's $store to its own $store
                 this.$store = this.$parent && this.$parent.$store;
             }
         },
     })
    }
    
    // Create class Store
    class Store {
     constructor(options) { // options receives the incoming store object
         this.vm = new Vue({
             // Make sure that state is responsive
             data: {
                 state: options.state
             }
         });
         // getter
         let getters = options.getters || {};
         this.getters = {};
         console.log(Object.keys(this.getters))
         Object.keys(getters).forEach(getterName => {
             Object.defineProperty(this.getters, getterName, {
                 get: () => {
                     return getters[getterName](this.state);
                 }
             })
         })
         // mutation
         let mutations = options.mutations || {};
         this.mutations = {};
         Object.keys(mutations).forEach(mutationName => {
             this.mutations[mutationName] = payload => {
                 mutations[mutationName](this.state, payload);
             }
         })
         // action
         let actions = options.actions || {};
         this.actions = {};
         Object.keys(actions).forEach(actionName => {
             this.actions[actionName] = payload => {
                 actions[actionName](this.state, payload);
             }
         })
     }
     // When getting state, return directly
     get state() {
         return this.vm.state;
     }
     // commit method, execute the 'name' method of changes
     commit(name, payload) {
         this.mutations[name](payload);
     }
     // The dispatch method executes the 'name' method of actions
     dispatch(name, payload) {
         this.actions[name](payload);
     }
    }
    
    // Expose the install method and class Store
    export default {
     install,
     Store
    }
  2. index.js

    // index.js
    import Vue from 'vue';
    import vuex from './vuex'; // Introduce vuex JS exposed object
    Vue.use(vuex); // It will execute the install method in the vuex object, that is, global mixin
    
    // Instance a Store class and expose it
    export default new vuex.Store({
     state: {
         num: 1
     },
     getters: {
         getNum(state) {
             return state.num * 2;
         }
     },
     mutations: { in (state, payload) {
             state.num += payload;
         },
         de(state, payload) {
             state.num -= payload;
         }
     },
     actions: { in (state, payload) {
             setTimeout(() => {
                 state.num += payload;
             }, 2000)
         }
     }
    })
  3. main.js

    // main.js
    import Vue from 'vue';
    import App from './App.vue'
    
    import store from './store/index'; // Introduce the index js
    
    
    new Vue({
     store, // Hang the store on the root instance
     el: '#app',
     components: {
         App
     },
     template: '<App/>',
    })

    So far, the state, variations, getter and actions of vuex are simply implemented. In the future, I will write an article on the implementation of mudule

vue doesn't advocate mixing mixins globally. Even mixins don't advocate using them. Don't use them indiscriminately!

epilogue

I'm Lin Sanxin, an enthusiastic front-end rookie programmer. If you are self-motivated, like the front end and want to learn from the front end, we can make friends and fish together. Ha ha, fish school, add me, please note [Si no]

Topics: Javascript Front-end ECMAScript Vue.js Interview