The realization principle of handwriting VEX4.0

Let’s start with a small vex4.0 use case

App.vue

<template>
  <div> count: {{ count }} | double: {{ double }} </div><! $store = $store = $store = $store<div> $store.state.count: {{ $store.state.count }} </div>
  <hr>
  <button @click="add">Synchronous change</button>
  <button @click="asyncAdd">Asynchronous modify</button>
</template>

<script>
import { computed, toRefs } from 'vue'
import { useStore } from 'vuex'

export default {
  name: 'App'.setup() {
    const store = useStore('store1')
    function add() {
      store.commit('add'.1)}function asyncAdd() {
      store.dispatch('asyncAdd'.1)}return {
      add,
      asyncAdd,
      / /... toRefs(store.state)
      // Implement responsiveness through computed listening, because count: store.state.count becomes assigned and does not have responsiveness
      count: computed(() = > store.state.count),
      double: computed(() = > store.getters.double)
    }
  }
}
</script>
Copy the code

store/index.js

import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 0
  },
  getters: {
    double(state) {
      return state.count * 2}},mutations: {
    add(state, payload) {
      state.count += payload
    }
  },
  actions: {
    asyncAdd({ commit }, payload) {
      setTimeout(() = > {
        commit('add', payload)
      }, 1000)}}})export default store
Copy the code

main.js

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
The use of vue.use (store) plugin calls the install method in store by default
createApp(App).use(store, 'store1').mount('#app')
Copy the code

CreateApp (App) returns an instance of vue. Its use method receives a store object containing Install. Install (vue, ‘store1’) vue instances with ‘store1’ as arguments to inject data from store objects into vue.

So how do we get the Store object? CreateStore ({}) createStore is a function that takes an object as an argument and returns a store object containing an install method.

[VuEX 0.1 is implemented as follows]

export function createStore(options) { // Options is the incoming configuration
  // todo options
  return {
      install(vue, injectKey){}}}Copy the code

The createStore method needs to do some complicated processing on the options passed in to return a fully functional store, so let’s write a class to handle options. Import {provide, inject} from ‘vue’ provide, inject The parent component has a provide option to provide data, and the child component has an Inject option to start using that data. See if this could be used to store vuex data

[VuEX 0.2 is implemented as follows]

import { inject } from 'vue' // inject: Get store data

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // Get the data in vuEX
  return inject(injectKey)
}

// This Store class is used to handle options
class Store {
  constructor(options) {
    // todo options
  }
  install(vue, injectKey = storeKey) {
    // The provide method of vue3 stores data in _provides by inject
    vue.provide(injectKey, this) InjectKey is the name of the store, and it is extracted by inject(injectKey)
    vue.config.globalProperties.$store = this // Add $store to vue3 instance (compatible with vue2)}}Copy the code

{vuex = useStore(‘store1’);}} {vuex = useStore(‘store1’);} We simply need to make the incoming state data reactive by means of Reactive (the reactive method in VUe3 enables the incoming object to be reactive)

[VuEX 0.3 is implemented as follows]

import { inject } from 'vue' // inject: Get store data

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // Get the data in vuEX
  return inject(injectKey)
}

class Store {
  constructor(options) {
    const store = this // Write this as store
    // Add one more layer of data to solve the problem of unresponsive reassignment of store._state. Data = XXX
    store._state = reactive({ data: options.state })
  }
  // Implement external access to responsive data via store.state
  get state() {
    return this._state.data
  }
  install(vue, injectKey = storeKey) {
    // The provide method of vue3 stores data in _provides by inject
    vue.provide(injectKey, this) InjectKey is the name of the store, and it is extracted by inject(injectKey)
    vue.config.globalProperties.$store = this // Add $store to vue3 instance (compatible with vue2)}}Copy the code

What about getters in Vuex? In store/index.js we find that getters is an object containing functions, whereas in app. vue we get the value directly from store.getters. Instead of store.getters.double() retrieving the returned value, how is this implemented? Think of object.defineProperty’s get method, which automatically returns a double() when a GET is triggered.

[VuEX 0.4 is implemented as follows]

import { reactive, inject } from 'vue' / / reactive: reactive | inject: access to store data

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) { // Get the data in vuEX
  return inject(injectKey)
}

class Store {
  constructor(options) {
    const store = this
    // Add one more layer of data to solve the problem of unresponsive reassignment of store._state. Data = XXX
    store._state = reactive({ data: options.state })
    
    // Implementing getters is simply implementing a calculated property
    const _getters = options.getters
    store.getters = {}
    forEachValue(_getters, function (fn, key) {
      Object.defineProperty(store.getters, key, {
        enumerable: true./ / can be enumerated
        get: () = > fn(store.state) // State caching can be implemented with computed packages})})}// Implement external access to data through store.state
  get state() {
    return this._state.data
  }
  install(vue, injectKey) {
    // The provide method of vue3 stores data in _provides by inject
    vue.provide(injectKey, this) InjectKey is the name of the store, and it is extracted by inject(injectKey)
    vue.config.globalProperties.$store = this // Add $store to vue3 instance (compatible with vue2)}}// This public method is used to iterate over the value and key of the object passed into the callback function
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key= > fn(obj[key], key))
}
Copy the code

The next step is to implement dispatch and commit. The key points of implementation are the first argument passed in and the this reference to the function when they are executed. Here we use the () => {} arrow function this to point to the property of the current scope and js’s call method to change the this pointer inside the function.

Bronze version Vex4.0

import { reactive, inject } from 'vue' / / reactive: reactive | inject: access to store data

export function createStore(options) {
  return new Store(options)
}

const storeKey = 'store'
export function useStore(injectKey = storeKey) {
  return inject(injectKey)
}

// Create a container that returns a store
class Store {
  constructor(options) {
    const store = this
    // Fix reassignment problem store._state.data = XXX
    store._state = reactive({ data: options.state })

    // Implementing getters is simply implementing a calculated property
    const _getters = options.getters
    store.getters = {}
    forEachValue(_getters, function (fn, key) {
      Object.defineProperty(store.getters, key, {
        enumerable: true.get: () = > fn(store.state) // State caching can be implemented with computed packages})})// Implement the Dispatch commit
    const _mutations = options.mutations
    const _actions = options.actions
    store._mutations = Object.create(null) // Why don't we create an empty object instead of {}, which would be simpler? You are wrong {} to create objects that have low prototype chain performance
    store._actions = Object.create(null)
    forEachValue(_mutations, function (mutation, key) {
      store._mutations[key] = (value) = > {
        mutation.call(store, store.state, value) The call method points the this of the mutation function to the store
      }
    })
    forEachValue(_actions, function (action, key) {
      store._actions[key] = (value) = > {
        action.call(store, store, value)
      }
    })
  }
  
  get state() {
    return this._state.data
  }
  
  Const {dispatch, commit} = useStore(); // The purpose of the arrow function is to point this to the current scope, that is, to the Store class: solve this pointing problem with const {dispatch, commit} = useStore()
  dispatch = (type, value) = > {
    this._actions[type](value)
  }
  commit = (type, value) = > {
    this._mutations[type](value)
  }
   
  Vue3 receives the install method to inject the store library into the project, and app is the imported root instance of vue3
  install(vue, injectKey = storeKey) {
    // The provide method of vue3 stores data in _provides by inject
    vue.provide(injectKey, this) InjectKey is the name of the store, and it is extracted by inject(injectKey)
    vue.config.globalProperties.$store = this // Add $store to vue3 instance (compatible with vue2)}}// This public method is used to iterate over the value and key of the object passed into the callback function
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key= > fn(obj[key], key))
}
Copy the code

All right, you’re done!

CodeSandbox effect demo