1. Motivation
In relatively independent components, the one-way data flow of action -> state -> view can be guaranteed. However, real-world business scenarios often require state passing and sharing. The general methods are:
-
State passing: Communication between parent and child components is done via
props(passing property values downwards, and passing methods upwards). For communication between sibling components, it's necessary to use events or lift the state to the parent level (converting the sibling communication problem into parent-child communication). -
State sharing: Either place it in one component and have other components find a way to get the state reference, or extract it as a singleton to be shared among components.
Deep props passing is quite painful, and interleaved event communication between sibling components brings maintenance issues. Lifting state to the parent level inflates the parent, causing it to manage too many detailed states. Putting shared state in one component makes it strenuous for other components to get the state reference. Extracting it as a singleton is slightly better, but having scattered shared state outside the component tree can also cause maintenance problems.
Extracting the state layer independently can effectively solve the problems of state passing and sharing. Using actions to add semantics to state changes not only alleviates maintenance issues but also brings debugging benefits.
2. Basic Principles
-
Application-level state is centrally managed by the
store. -
The only way to modify state is to
commita synchronousmutation. -
Asynchronous logic is placed in
actions.
It agrees with an easily manageable single state tree and standardizing the way state is modified. In addition, it is closer to business needs by considering asynchronous scenarios in its design.
3. Structure
Unlike Redux, which is a bit strange (the reducer seems unrelated to Flux at first glance), Vuex feels more like a conventional Flux implementation:
component View layer, dispatches actions
---
action Event layer, commits mutations
Asynchronous: Unified management of asynchronous requests
---
mutation Response layer, mutates state
Synchronous: Logically atomic state modifications
---
state Data model layer, updates model
Maps to view updates via data binding
Among them, mutation and action are globally shared, which also solves the component communication problem (no need to manually pass state; just tell the store what happened, and the store knows what to do). This avoids lifting/passing state and brings semantic benefits.
Global sharing inevitably brings the problem of naming conflicts, so Vuex also provides a namespacing option.
Comparison with Flux
Generates action Passes action update state
view interaction -----------> dispatcher -----------> stores --------------> views
You can see that the biggest difference is that Vuex subdivides action into action and mutation to handle asynchronous and synchronous scenarios respectively. The store itself acts as the dispatcher (responsible for registering/dispatching actions/(mutations)).
In other words, if you consider action and mutation as one layer (the action in Flux), their structures are completely identical. That's why Vuex is considered a more conventional Flux implementation.
store
Acts as a container for state and additionally serves as a dispatcher.
Using the store to manage state is functionally equivalent to global.share = {}, but store.state in Vuex has some other characteristics:
-
stateis reactive data. -
Direct modification of the
stateheld by thestoreis not allowed; you must explicitlycommit mutation.
Similar to a component's data, store.state is also reactive. It is linked with the component's computed properties, and state updates are precisely propagated to the view layer.
Not allowing direct modification of store.state is also a moral constraint. Although it will throw an error if the strict option is enabled, the modification actually takes effect. The lack of a strong constraint (write protection) here might be due to market considerations.
Additionally:
-
Single state tree, identical to Redux. It provides an extra modularity mechanism to manage (split/organize)
state. -
Similarly, it is not required to stuff all
stateinto Vuex; it is recommended to maintain relatively independent state at the component level.
getter
Functionally equivalent to computed properties of the store.
Used to wrap state, packaging raw state (performing simple calculations on store.state, like filter, count, find, etc.) into a format required for view display.
Without getters, this weak logic would have to be placed either in computed or in the template. Providing getters allows extracting everything related to state.
mutation
Responsible for updating state. mutations are all synchronous operations; the line after a commit mutation, the state update is complete.
Pre-registered in the store. Each commit looks up the mutation table and executes the corresponding state update function.
Note that mutations must be synchronous, otherwise, debugging tools won't be able to capture correct state snapshots (if the state is modified asynchronously), which would break state tracking.
action
Used to handle asynchronous scenarios, acting as a supplement to mutations.
Vuex essentially divides Flux's action into synchronous mutations and asynchronous actions.
actions do not directly modify state like mutations do. Instead, they modify it indirectly by committing mutations. That is to say, only mutations correspond to atomic state update operations.
actions can contain asynchronous operations. By design, asynchronous operations are intentionally separated into actions, keeping mutations synchronous.
Asynchronous Flow Control
Asynchronous flow control can be resolved by having actions return promises, which is more elegant than passing callback functions.
In Vuex v2.x (currently latest v2.3.0 as of 2017/7/1), store.dispatch returns a promise by default. Non-promise action return values will be wrapped into a promise via Promise.resolve().
dispatch(type: string, payload?: any, options?: Object) | dispatch(action: Object, options?: Object)
Dispatch an action.
optionscan haveroot: truethat allows to dispatch root actions in namespaced modules. Returns a Promise that resolves all triggered action handlers.
(Excerpt from API Reference)
But this is meaningless for asynchronous operations (Promise.resolve(undefined)). If you need to control the asynchronous flow, you should still manually return a promise and pass out the required information from the inner promise.
module
Modularity mechanism, used to split and organize the store.
Provides a namespaced option, which uses the module path as a prefix upon registration. It's a very exquisite design: it smooths out the impact of namespaces by injecting local.dispatch/commit/getters/state into the module. Inside the module, namespaces aren't needed; outside the module (business code or other modules), namespaces are required. Thus, the namespace becomes a toggle option, having no impact on the store part.
4. Tools
Similarly, Vuex also needs to handle the state -> view part (functioning similarly to react-redux, connecting the state management layer to the view layer).
Vue, which supports precise data binding, doesn't need to be as troublesome as React (inserting some containers into the virtual DOM tree to propagate store.state changes). It just needs to connect store.state with the component state. Like a soft link, the component shares the state object with the store, and state changes are passed to the component via reactive properties, updating the view.
mapState
Connects store.state with the component's computed properties.
Note: mapState can forcibly prohibit directly modifying computed properties in a component to affect actual state (the computed properties generated by mapState are read-only).
{
configurable: true,
enumerable: true,
get: function computedGetter(),
set: function noop()
}
mapGetters
Connects store.getter with the component's computed properties.
Similar to mapState, it also has write protection.
mapMutations
Connects mutations with the component's methods.
Simplifies the process of a component committing mutations (requires injecting store at the top level).
mapActions
Connects actions with the component's methods.
Simplifies the process of dispatching actions (also requires injecting store).
5. Questions
1. How to avoid sharing state among identical components?
For example, if there are 3 identical components in a list, how do you avoid the state consistency problem caused by sharing state?
Conflict between module reuse and state sharing. Similar to handling data, use a function for state that returns a new state object, instead of using a state object directly. This way, the state (a small piece of store.state) for each of the 3 components is independent, and no extra state management is needed.
Note: The function state feature is available in Vuex v2.3.0+. For lower versions, you need to consider other methods, such as:
-
Elevating the state by one level (maintaining an array to manage a
state list). -
Considering putting unsharable local state at the component level, and putting sharable data and operations into the
store.
The first method causes the store to bloat rapidly, and actions/mutations all require an index. The component needs to pass the index back to the store, which is too troublesome and inadvisable.
The second method is the ultimate solution. State partitioning techniques apply everywhere; do not use Vuex purely for the sake of using Vuex. While it's not impossible to extract all state from components and put it in the store, if the state held by the store is too granular, it will be a huge hassle for development and maintenance:
-
During development, any minute change in a component requires a
dispatch/commit. -
During maintenance, you will face a very complex
storewith thousands ofmutation types.
And this trouble is entirely self-inflicted. So, consider how state should be partitioned:
-
Interaction-related UI state should be placed at the component level. Examples: expand/collapse,
loadingshow/hide,tabs/table pagination, etc. -
Unsharable data state should be placed at the component level. Example: form input data.
-
Sharable data state should be placed in the state layer. Example: cacheable service data.
The role of the store should be server + database, existing as a front-end data layer, rather than simply extracting application state from the component tree to form a state tree, which isn't very meaningful.
2. How are computed properties linked to vuex's store.state?
Runtime dependency collection mechanism.
// Component
computed: {
user() {
return this.$store.state.user;
}
}
// store
mutations: {
[types.SET_USER] (state, user) {
state.user = user;
}
}
When calculating each computed property, executing the user() function accesses store.state.user, triggering the getter of the state, and records the information that the user() function depends on store.state.user.
Later, if a commit mutation modifies store.state.user, it triggers the setter of the state and re-evaluates all dependencies (including the user() function) of the user property.
Then it triggers the setter of the computed property, executes all dependencies corresponding to computed.user (including the view update function), and the view update is completed.
P.S. For the specific implementation of the dependency collection mechanism, see vue/src/core/observer/dep.js
3. store passing mechanism
Similar to react-redux's Provider, it also provides a way to inject it globally at once (Vue.use(Vuex) and passing store when new-ing the top-level component).
Vuex acts as a plugin, modifying Vue.prototype to attach $store to it, allowing all vms to share it.
4. Conflict between two-way binding scenarios like inputs and store.state not being directly modifiable
Handled through the getter/setter of computed properties:
-
Read
store.statein thegetter. -
commit mutationto write tostore.statein thesetter.
No comments yet. Be the first to share your thoughts.