This is the 7th day of my participation in the August Text Challenge.More challenges in August

Vuex introduction

Let’s start by introducing some of the concepts in VUex

Let’s follow vuEX’s workflow

  • changestateThe only way is to submitmutations
  • If asynchronous, dispatch.actionsThe essence is submissionmutations
  • submitmutationsAfter that, components can be dynamically renderedVue Components

Is there something missing? Yes, is there something missing

Vuex implementation

Now let’s implement a simple version of Vuex, modeled after vuex’s source code

You can check out the full source code here, along with the article

Building a Store

First we need to create a store with the following properties and methods

Define the createStore method, which simply creates a Store instance and passes options

export function createStore(options) {
  return new Store(options)
}
Copy the code

Let’s look at the implementation of the Store class

The process is shown in figure

export class Store {
  constructor(options = {}) {
    const plugins = options.plugins || []
    this._subscribers = []
    this._actionSubscribers = []
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this.getters = Object.create(null)

    / / collection modules
    this._modules = new ModuleCollection(options)

    // Bind commit and dispatch to itself
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit(type, payload) {
      return commit.call(store, type, payload, options)
    }

    const state = this._modules.root.state

    / / install the module
    installModule(this, state, [], this._modules.root)

    // Initialize state
    resetStoreState(this, state)

    // Application plug-in
    plugins.forEach(plugin= > plugin(this))}}Copy the code

Collection of modules

Modules are collected using ModuleCollection to generate a module tree

// store.js
export class Store {
  constructor(options = {}) {
      this._modules = new ModuleCollection(options)
  }
}

// module-collection.js
export default class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule)
  }
    
  register(path, rawModule){}}Copy the code

A register function is called in the ModuleCollection to register each module

The Path argument refers to the current Moduel road dynamics, which can be used to determine hierarchies, and the rawModule is the original Module object

  register(path, rawModule) {
    const newModule = new Module(rawModule)
    if (path.length === 0) {
      // Root is defined as rawModule
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // If modules are passed in, recursively register the submodules
    if (rawModule.modules) {
      Object.keys(rawModule.modules).forEach(key= > {
        const rawChildModule = rawModule.modules[key]
        this.register(path.concat(key), rawChildModule)
      })
    }
  }
Copy the code

In register, through rawModule we instantiate a Module object that has the following properties

  • _rawModule: The original object passed in
  • _childrenSon: moduels
  • state: The state value of the original object passed in
  • getChild: Ways to get sub-Moduel
  • addChild: Method to add child modules
export default class Module {
  constructor(rawModule) {
    this._rawModule = rawModule
    this._children = Object.create(null)
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}
Copy the code

When path.length === 0, use this newModule as the root value, which can also be called the parent Module

After root is set, it continues to determine if modules exist and, if so, recursively registers submodules

    // If modules are passed in, recursively register the submodules
    if (rawModule.modules) {
      Object.keys(rawModule.modules).forEach(key= > {
        const rawChildModule = rawModule.modules[key]
        this.register(path.concat(key), rawChildModule)
      })
    }
Copy the code

In this case, set the key of each module to path to separate the hierarchy and add it to root

get(path) {
    return path.reduce((module, key) = > {
      return module.getChild(key)
    }, this.root)
  }

if (path.length === 0) {
  // ...
} else {
  const parent = this.get(path.slice(0, -1))
  parent.addChild(path[path.length - 1], newModule)
}
Copy the code

We end up with a module tree like this

Bind commit and Dispatch

Go back to the store constructor code

    // Bind commit and dispatch to itself
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit(type, payload) {
      return commit.call(store, type, payload, options)
    }
Copy the code

Encapsulate the dispatch and Commit methods in the replacement prototype, pointing this to the current Store object. The dispatch and commit methods are implemented as follows:

  commit(type, payload) {
    const mutation = { type, payload }
    const entry = this._mutations[type]
    if(! entry) {return
    }
    entry(payload)
    this._subscribers.slice().forEach(sub= > sub(mutation, this.state))
  }
Copy the code

We mentioned at the beginning that changing state requires a mutation commit, which implements this process

Dispath performs the same function as COMMIT, except that disPATH dispatches an action to submit the mutation state. We usually perform asynchronous functions in the action

  dispatch(type, payload) {
    const action = { type, payload }
    const entry = this._actions[type]
    if(! entry) {return
    }
    try {
      this._actionSubscribers
        .slice()
        .filter(sub= > sub.before)
        .forEach(sub= > sub.before(action, this.state))
    } catch (error) {
      console.error(e)
    }

    const result = entry(payload)

    return new Promise((resolve, reject) = > {
      result
        .then(res= > {
          try {
            this._actionSubscribers
              .filter(sub= > sub.after)
              .forEach(sub= > sub.after(action, this.state))
          } catch (error) {
            console.error(e)
          }
          resolve(res)
        })
        .catch(error= > {
          try {
            this._actionSubscribers
              .filter(sub= > sub.error)
              .forEach(sub= > sub.error(action, this.state, error))
          } catch (e) {
            console.error(e)
          }
          reject(error)
        })
    })
  }
Copy the code

In commit and DisPATH, each subscription function set _subscribers and _actionSubscribers is executed,

In the subscription function of _SUBSCRIBERS is passed the current mutation object and the current state, which are the parameters provided to the plug-in

_actionSubscribers also divides functions into before, After, and error types

The module is installed

The Module is installed to encapsulate the mutations, actions, and getters functions, passing in the required parameters

Encapsulation mutation

  if (module._rawModule.mutations) {
    Object.keys(module._rawModule.mutations).forEach(key= > {
      const mutation = module._rawModule.mutations[key]
      store._mutations[key] = payload= >
        // Lazy get state
        mutation.call(store, getNestedState(store.state, path), payload)
    })
  }
Copy the code

To encapsulate the action

  if (module._rawModule.actions) {
    Object.keys(module._rawModule.actions).forEach(key= > {
      const action = module._rawModule.actions[key]
      store._actions[key] = payload= > {
        let res = action.call(
          store,
          {
            dispatch: store.dispatch,
            commit: store.commit,
            getters: store.getters,
            state: getNestedState(store.state, path),
          },
          payload
        )
        if(! (resinstanceof Promise)) {
          res = Promise.resolve(res)
        }
        return res
      }
    })
  }
Copy the code

Encapsulation getter

  if (module._rawModule.getters) {
    Object.keys(module._rawModule.getters).forEach(key= > {
      const getter = module._rawModule.getters[key]
      store.getters[key] = () = >
        // Lazy get state
        getter(getNestedState(store.state, path), store.getters)
    })
  }
Copy the code

Finally, recursively install the child Modules

  Object.keys(module._children).forEach(key= >
    installModule(store, rootState, path.concat(key), module._children[key])
  )
Copy the code

It is important to note that state retrieval needs to be lazy, because state can change during vuEX use, and if you fix state when encapsulating functions, you will have unexpected behavior

Initialize the state

And then the resetStoreState function

export function resetStoreState(store, state) {
  store._state = reactive({
    data: state,
  })
}
Copy the code

Make state reactive so that the view can be updated after Vuex changes state

The data responsiveness principle of VUe3 can be seen in my article analysis of the data responsiveness principle of VUE3

You might be a little confused by this, but why is there _state on the instance instead of state? There’s actually a getter in the store that gets the value of state

  get state() {
    return this._state.data
  }

  set state(v) {}
Copy the code

Here we can also see that setting state directly does not work

Application of plug-in

Let’s start by implementing a plug-in that prints changes before and after changing state: Logger

export const logger = store= > {
  let prevState = deepClone(store.state)

  store.subscribe((mutation, state) = > {
    const nextState = deepClone(state)

    const formattedTime = getFormattedTime()
    const message = `${mutation.type}${formattedTime}`
    console.log('%c prev state'.'color: #9E9E9E; font-weight: bold', prevState)
    console.log('%c mutation'.'color: #03A9F4; font-weight: bold', message)
    console.log('%c next state'.'color: #4CAF50; font-weight: bold', nextState)

    prevState = nextState
  })
}
Copy the code

Listen for mutation

Each plug-in is injected in the Store

 plugins.forEach(plugin= > plugin(this)
Copy the code

To test the effect, if you clone the source code, you can execute it after installing the dependencies

yarn dev
Copy the code

Open the console to see the effect

Refer to the article

Vuex framework principle and source code analysis