Skip to main content

Vuex

Free2017-07-01#JS#Vuex与Redux#Vuex与Flux#vuex组件状态共享#vuex解析#vuex实现机制

Clicking all the way here from Flux and Redux

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 commit a synchronous mutation.

  • 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:

  • state is reactive data.

  • Direct modification of the state held by the store is not allowed; you must explicitly commit 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 state into 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. options can have root: true that 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 store with thousands of mutation 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, loading show/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.state in the getter.

  • commit mutation to write to store.state in the setter.

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment