As we all know, Vuex is an implementation of the Flux architecture. Flux clearly establishes various functional units in data management scenarios, and its main criteria are as follows:

  1. Centralized state management
  2. Status can only be passed exclusivelymutationCell changes
  3. The application layer triggers changes by sending signals (commonly called actions)

Vuex was also developed closely around these guidelines, providing the core functionality of Flux patterns through the Store class. In addition to meeting the basic requirements of the architecture, a number of further facilitation measures have been designed:

  1. Isolation of data units through “modular” design
  2. Provides getter mechanisms to improve code reuse
  3. useVue.$watchMethod to implement data flow
  4. Zero configuration, naturally integrated into the Vue environment

There are plenty of analytical articles on the web, so there’s no need to go over them. This paper only focuses on the implementation of centralization, signal mechanism and data flow, and discusses the defects of Vuex implementation.

centralized

In Vuex, Store integrates all functions, serves as the main interface provided by external devices, and serves as the data management center in Flux mode. Through it, Vuex provides:

  • Signal-dependent:Dispatch, commit
  • Listener interface:subscribe
  • State value change interface (replace state value, should not be called) :replaceState
  • State model change interface (recommended only for on-demand reference scenarios) :RegisterModule, unregisterModule
  • Hot update interface (HMRLogic, not concerned) :hotUpdate

The officially implemented Store is very complex and has a lot of coupled logic. For simplicity, we removed all kinds of bypass logic and focused only on the centralized and signal control mechanism of Flux architecture, and concluded a very simple implementation:

export default class Store {
  constructor(options) {
    this._state = options.state;
    this._mutations = options.mutations;
  }

  get state() {
    return this._state;
  }

  commit(type, payload) {
    this._mutations[type].apply(this[this.state].concat([...payload])); }}Copy the code

This is the core of understanding Vuex. There are only two pieces of logic in the whole code:

  1. through_stateProperties implement a centralized, self-contained data center layer.
  2. throughdispatchMethod that triggers a callback prior to registration_mutationsMethods.

There are many problems with this code, for example:

  • Use simple objects as state
  • A state mutation is achieved only by modifying the value of the state object property
  • There is no effective mechanism to prevent state objects from being modified by mistake

These design issues also exist in Vuex, and are closely related to vuue.$watch (see below), which I personally think is extremely lax.

Signal mechanism

Vuex provides two signal-related interfaces, whose source code can be abbreviated as:

export default class Store {... commit (_type, _payload, _options) { ... const entry =this._mutations[type]
    this._withCommit((a)= > {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub= > sub(mutation, this.state))
    ...
  }

  dispatch (_type, _payload) {
    ...
    const entry = this._actions[type]
    return entry.length > 1
      ? Promise.all(entry.map(handler= > handler(payload)))
      : entry[0](payload)
  }
  ...
}
Copy the code

The difference between the two is:

  1. dispatchTrigger isactionThe callback.commitThe triggermutationThe callback.
  2. dispatchReturns the Promise;commitNo return value.

This design intention, mainly separation of responsibilities, the action unit is used to describe what happened; Mutation is used to modify the data layer state. Vuex uses a similar interface to place the two on the same position. In fact, this layer of interface design has drawbacks:

  1. Action and mutation each need a type system
  2. Allows the application layer to bypass actions directlycommit mutation
  3. The state is notimmutableAnd changes are allowed in the actionstate

While this does increase convenience, for starters, it can lead to the following antipatterns:

  • Two type systems that cannot be orthogonal are designed
  • The illusion of “directly submitting mutation” was caused, and the signaling mechanism of Flux was destroyed
  • Hand changes state by mistake in action without friendly trace mechanism (especially in getters)

Since there is no exact mechanism to prevent errors, you need to be very, very vigilant when using Vuex; The need for rigorous and correct use of functional units; Or fill design gaps with specifications.

Unidirectional data flow

The data flow here refers to the mapping from the state of Vuex to the props/computed/data and other state units of the Vue component, that is, how to get state in the component. Vuex officially recommends using mapGetter and mapState interfaces to implement data binding.

mapState

The function is very simple and the code logic can be summarized as follows:

export const mapState = normalizeNamespace((namespace, states) = > {
  const res = {}
  ...
  normalizeMap(states).forEach(({ key, val }) = > {
    res[key] = function mappedState() {... returntypeof val === 'function' ?
        val.call(this, state, getters) :
        state[val]
    }
  })
  ...
  return res
})
Copy the code

MapState reads the properties of the state object directly. It is worth noting that res[key] is typically mounted on an external object as a function, in which case the function’s this points to the mounted Vue component.

mapGetter

This function is also very simple and its code logic is as follows:

export const mapGetters = normalizeNamespace((namespace, getters) = > {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) = > {

    res[key] = function mappedGetter() {... returnthis.$store.getters[val]
      }
      ...
  })
  return res
})
Copy the code

MapGetter accesses the getters property of the $Store instance that the component mounts.

From the state to the getter

Vuex’s getter property is very similar to Vue’s computed property in all respects, and in fact, the getter is based on computed. Its core logic is as follows:

function resetStoreVM(store, state, hot) {... store.getters = {}const wrappedGetters = store._wrappedGetters
  const computed = {}
    // Iterate through the getter configuration to generate computed properties
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = (a)= > fn(store)
    Object.defineProperty(store.getters, key, {
      // Get vue instance attributes
      get: (a)= > store._vm[key],
      enumerable: true // for local getters})})// Create a new Vue instance to listen for property changes
  store._vm = new Vue({
      data: {
        ? state: state
      },
      computed
    })
    ...
}
Copy the code

As you can see from the code, Vuex hosts the entire state object into the data property of the Vue instance in exchange for vue’s entire Watch mechanism. The getter property is implemented in a really neat way, using a computed property that returns an instance. The question is:

  1. Vuex is deeply coupled to Vue and cannot be transferred to other environments
  2. The VuewatchThe mechanism is implemented based on the attribute read and write function. If the root node is directly replaced, the subattribute callback will fail, that is, it is impossible to implementimmutablefeatures

After the language

Vuex gives me the biggest feeling: convenience, the same function has a variety of semantic lUS processing, the separation of responsibilities is very good, if strictly follow the specification, can really organize the code very well; The interface is also very straightforward and developer friendly. In terms of the number of users, influence, and so on, it is undoubtedly a very great framework. Of course, some of the views put forward here also differ from person to person, and the purpose is nothing more than to attract jade.