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:
- Centralized state management
- Status can only be passed exclusively
mutation
Cell changes - 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:
- Isolation of data units through “modular” design
- Provides getter mechanisms to improve code reuse
- use
Vue.$watch
Method to implement data flow - 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:
- through
_state
Properties implement a centralized, self-contained data center layer. - through
dispatch
Method that triggers a callback prior to registration_mutations
Methods.
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:
dispatch
Trigger isaction
The callback.commit
The triggermutation
The callback.dispatch
Returns the Promise;commit
No 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:
- Action and mutation each need a type system
- Allows the application layer to bypass actions directly
commit
mutation - The state is not
immutable
And 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:
- Vuex is deeply coupled to Vue and cannot be transferred to other environments
- The Vue
watch
The 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 implementimmutable
features
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.