State
Single state tree
Vuex uses a single state tree — yes, it contains all the application-level state in a single object. So far it has been a “unique data source (SSOT)”. This also means that each app will contain only one Store instance. A single state tree allows us to directly locate any particular state fragment and easily take a snapshot of the entire current application state during debugging.
Single-state trees and modularity are not in conflict — we will discuss how to distribute states and state-change events across submodules in a later section.
Data stored in Vuex follows the same rules as data in Vue instances, such as that the state object must be plain. Vue#data (opens new window).
Get the Vuex state in the Vue component
So how do we present state in Vue components? Since Vuex’s state store is reactive, the easiest way to read state from a Store instance is to return some state in a compute property:
// Create a Counter component const Counter = {template: '<div>{{count}}</div>', computed: { count () { return store.state.count } } }Copy the code
Every time store.state.count changes, the calculated property is refetched and an update to the associated DOM is triggered.
However, this pattern causes components to rely on global state singletons. In a modular build system, state needs to be imported frequently in each component that needs to use state, and state needs to be simulated when testing components.
Vuex, via the Store option, provides a mechanism to “inject” state from the root component into each child component (by calling vue.use (Vuex)) :
Const app = new Vue({el: '#app', // Provide the store object to the 'Store' option, which can inject instances of store into all child stores, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` })Copy the code
By registering the Store option in the root instance, the store instance is injected into all the children of the root component, which can be accessed through this.$store. Let’s update the implementation of Counter:
mapState
Auxiliary function
When a component needs to fetch multiple states, it can be repetitive and redundant to declare all those states as computed properties. To solve this problem, we can use the mapState helper function to help us generate calculated properties that will save you from pressing the key:
MapState import {mapState} from 'Vuex' export default {//... Computed: mapState({// Arrow function makes code more concise count: State => state.count, // Pass string argument 'count' equal to 'state => state.count' countAlias: CountPlusLocalState (state) {return state.count + this.localcount}})}Copy the code
We can also pass mapState an array of strings when the name of the computed property of the map is the same as the name of the child node of State.
Computed: mapState([// map this.count to store.state.count 'count'])Copy the code
Object expansion operator
The mapState function returns an object. How do we mix it with local computed properties? Typically, we need to use a utility function to merge multiple objects into one so that we can pass the final object to the computed property. But since the object expansion operator opens new Window, we can make it much simpler:
computed: { localComputed () { /* ... */}, // Use the object expansion operator to blend this object into an external object... mapState({ // ... })}Copy the code
The component still retains local state
Using Vuex does not mean that you need to put all the states into Vuex. While putting all the state in Vuex makes state changes more explicit and easier to debug, it also makes the code tedious and unintuitive. If there are states that belong strictly to a single component, it is best to treat them as local states of the component. You should make trade-offs and decisions based on your application development needs.
Getter
Sometimes we need to derive some state from the state in the store, such as filtering and counting lists:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
Copy the code
If more than one component needs this property, we can either copy the function or extract a shared function and import it in multiple places — neither approach is ideal.
Vuex allows us to define “getters” (you can think of them as computed properties of the store) in the store. Just like evaluating properties, the return value of a getter is cached based on its dependency and is recalculated only if its dependency value changes.
The Getter accepts state as its first argument:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
Copy the code
Access by property
Getters are exposed as store.getters objects, and you can access these values as properties:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Copy the code
Getters can also accept other getters as second arguments:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
Copy the code
store.getters.doneTodosCount // -> 1
Copy the code
We can easily use it in any component:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
Copy the code
Note that getters are cached as part of Vue’s responsive system when accessed through properties.
Access by method
You can also pass parameters to a getter by asking the getter to return a function. It’s useful when you’re querying an array in a store.
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
Copy the code
store.getters.getTodoById(2) // -> { id: 2, text: '... ', done: false }Copy the code
Note that the getter is called every time it is accessed through a method, without caching the result.
mapGetters
Auxiliary function
The mapGetters helper function simply maps the getters in the store to local computed properties:
}
Copy the code
If you want to give a getter property another name, use object form:
. MapGetters ({/ / the ` enclosing doneCount ` mapping for ` enclosing $store. The getters. DoneTodosCount ` doneCount: 'doneTodosCount})Copy the code
Mutation
The only way to change the state in Vuex’s store is to commit mutation. Mutations in Vuex are very similar to events: each mutation has a string event type (type) and a callback function (handler). This callback is where we actually make the state change, and it takes state as the first argument:
Const store = new Vuex. Store ({state: {count: 1}, mutations: {increment (state) {/ / the status state. Count++}}})Copy the code
You cannot call a mutation handler directly. This option is more like event registration: “This function is called when a mutation of type INCREMENT is triggered.” To wake up a mutation handler, you need to call the store.mit method with the corresponding type:
store.commit('increment')
Copy the code
Payload submission
You can pass an additional parameter, payload, to store.mit:
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
Copy the code
store.commit('increment', 10)
Copy the code
In most cases, the payload should be an object, which can contain multiple fields and the mutation recorded will be more readable:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Copy the code
store.commit('increment', {
amount: 10
})
Copy the code
Object style submission
Another way to submit mutation is to use an object containing the type attribute directly:
store.commit({
type: 'increment',
amount: 10
})
Copy the code
When using an object-style commit, the entire object is passed as a payload to the mutation function, so the handler remains unchanged:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Copy the code
The Mutation complies with the Vue response rules
Since the state in Vuex’s Store is responsive, the Vue component that monitors the state updates automatically when we change the state. This also means that mutation in Vuex requires the same precautions as mutation in Vue:
- It is best to initialize all required properties in your store in advance.
- When you need to add new properties to an object, you should
- use
Vue.set(obj, 'newProp', 123)
Or, - Replace an old object with a new one. For example, using the object expansion operator, we can write:
state.obj = { ... state.obj, newProp: 123 }Copy the code
Replace Mutation event types with constants
Substituting constants for mutation event types is a common pattern in various Flux implementations. This allows tools like Linter to work, and keeping these constants in a separate file allows your code collaborators to see what mutations are included in the entire app:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
Copy the code
// store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... Mutations: {// we can use ES2015 style computing attribute naming to use a constant as the function name [SOME_MUTATION] (state) {// mutate state}}})Copy the code
Whether or not you use constants is up to you — this can be very helpful on large projects that require multiple people to work together. But if you don’t like it, you don’t have to.
Mutation must be a synchronization function
An important rule to remember is that mutation must be a synchronization function. Why is that? Please refer to the following example:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
Copy the code
Now imagine that we are debugging an app and looking at the mutation log in devtool. Each mutation is recorded, and DevTools needs to capture a snapshot of the previous state and the next state. However, the callback in the asynchronous function in mutation in the example above made this impossible: Because the callback had not yet been called when mutation was triggered, DevTools did not know when the callback was actually called — essentially any state change made in the callback was untraceable.
Commit Mutation in the component
You can use this. codeStore.mit (‘ XXX ‘) to commit mutation in the component, or use the mapMutations helper function to map methods in the component to a store.mit call (requiring store injection at the root node).
import { mapMutations } from 'vuex' export default { // ... methods: { ... Apply mutations ([' increments ', // map 'this.increment()' to 'this.store.com MIT (' increments ')' 'incrementBy' // Map 'this.incrementBy(amount)' to 'this.incrementBy(amount)'),... Apply mutations ({add: 'increment' // map 'this.add()' to 'this.store.com MIT ('increment')'})}}Copy the code
Next step: Action
Mixing asynchronous calls in mutation can make your program difficult to debug. For example, when you call two mutations containing asynchronous callbacks to change the state, how do you know when to call back and which to call first? That’s why we have to distinguish between these two concepts. In Vuex, mutation are all synchronous transactions:
Store.com MIT ('increment') // Any state change caused by increment should be completed at this moment.Copy the code
To handle asynchronous operations, let’s look at actions.
Action
Action is similar to mutation, except that:
- The Action commits mutation rather than a direct state change.
- Actions can contain any asynchronous operation.
Let’s register a simple action:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Copy the code
The Action function accepts a context object with the same methods and properties as the store instance, so you can submit a mutation by calling context.mit. Or get state and getters via context.state and context.getters. When we cover Modules later, you’ll see why the context object is not the Store instance itself.
In practice, we’ll often use ES2015’s parameter deconstruction to simplify code (especially if we need to call commit many times) :
actions: {
increment ({ commit }) {
commit('increment')
}
}
Copy the code
Distribution of the Action
Action is triggered by the store.dispatch method:
store.dispatch('increment')
Copy the code
At first glance, it seems unnecessary. Wouldn’t it be easier to just distribute mutation? In fact, this is not the case. Remember that mutation must implement this limitation synchronously? Action is not bound! We can perform asynchronous operations within an action:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Copy the code
Actions support the same payloads and objects for distribution:
// Distribute store.dispatch('incrementAsync', {amount: 10}) // distribute store.dispatch({type: 'incrementAsync', amount: 10})Copy the code
Let’s look at a more practical shopping cart example that involves calling the asynchronous API and distributing multiple mutations:
actions: {checkout ({commit, state}, products) {const savedCartItems = [...state.cart.added] Commit (types.checkout_request) // The shopping API accepts a successful callback and a failed callback shop.buyProducts(products, CHECKOUT_SUCCESS => commit(types.checkout_failure, savedCartItems)}}Copy the code
Note that we are doing a series of asynchronous operations and recording side effects (that is, state changes) from the action through mutation.
Distribute the Action in the component
You use this.$store.dispatch(‘ XXX ‘) to distribute actions in the component, or use the mapActions helper function to map the component’s methods to a store.dispatch call (which requires injecting store at the root node first) :
import { mapActions } from 'vuex' export default { // ... methods: { ... MapActions (['increment', // Map 'this.increment()' to 'this.$store.dispatch('increment')' // 'mapActions' also supports payloads: 'incrementBy' // map 'this.incrementBy(amount)' to 'this.$store.dispatch('incrementBy', amount)']),... MapActions ({add: 'increment' // map 'this.add()' to 'this.$store.dispatch('increment')'})}}Copy the code
Combination of the Action
Actions are usually asynchronous, so how do you know when an Action ends? More importantly, how can we combine multiple actions to handle more complex asynchronous processes?
First, you need to understand that store.dispatch can handle the Promise returned by the handler of the triggered action, and that store.dispatch still returns promises:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
Copy the code
Now you can:
store.dispatch('actionA').then(() => {
// ...
})
Copy the code
This is also possible in another action:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
Copy the code
Finally, if we use async/await (open new window), we can compose actions as follows:
// Suppose getData() and getOtherData() return Promise actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, Commit}) {await dispatch('actionA') // await actionA to complete commit('gotOtherData', await getOtherData())}}Copy the code
A single store.dispatch can trigger multiple action functions in different modules. In this case, the returned Promise will not be executed until all triggering functions have completed.
Module
Because of the use of a single state tree, all the states of an application are grouped into one large object. When the application becomes very complex, the Store object can become quite bloated.
To solve these problems, Vuex allows us to split the Store into modules. Each module has its own state, mutation, action, getter, and even nested submodules — split the same way from top to bottom:
const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: ModuleB}}) Store.state. a // -> moduleA status Store.state. b // -> moduleB statusCopy the code
Local state of a module
For mutation and getters inside a module, the first argument received is the module’s local state object.
const moduleA = { state: () => ({ count: 0 }), mutations: {increment (state) {// where the 'state' object is the local state of the module state.count++}}, getters: { doubleCount (state) { return state.count * 2 } } }Copy the code
Similarly, for actions within the module, the local state is exposed through context.state and the rootState is context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
Copy the code
For getters inside the module, the root node state is exposed as a third parameter:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
Copy the code
The namespace
By default, actions, mutations, and getters inside a module are registered in the global namespace — enabling multiple modules to respond to the same mutation or action.
If you want your modules to be more wrapped and reusable, you can make them namespaced by adding namespaced: True. When a module is registered, all its getters, actions, and mutations are automatically named according to the path the module was registered with. Such as:
Const store = new vuex.store ({modules: {account: {namespaced: true, // Module assets) state: () => ({... }), // module states are already nested, and using the 'namespaced' attribute doesn't affect them. Getters: {isAdmin () {... } // -> getters['account/isAdmin'] }, actions: { login () { ... } // -> dispatch('account/login') }, mutations: { login () { ... } / / - > commit (' account/login ')}, / / nested modules modules: {myPage / / father module namespace: {state: (a) = > ({... }), getters: { profile () { ... } // -> getters['account/profile']}}, // Further nested namespace posts: {namespaced: true, state: () => ({... }), getters: { popular () { ... } // -> getters['account/posts/popular'] } } } } } })Copy the code
Namespace-enabled getters and actions receive localized getters, dispatch, and commit. In other words, you do not need to add a space name prefix to the same module when using module assets. Changing the Namespaced attribute does not require modifying the code in the module.
Accessing Global Assets within a module with a namespace
If you want to use the global state and getter, rootState and rootGetters are passed to the getter as the third and fourth arguments, and the action is passed to the context object’s properties.
To distribute action or commit mutation within the global namespace, pass {root: true} as the third argument to Dispatch or COMMIT.
modules: { foo: { namespaced: true, getters: {// In the getter of this module, 'getters' is localized // You can call' rootGetters' someGetter (state, getters, rootState, rootGetters) { getters.someOtherGetter // -> 'foo/someOtherGetter' rootGetters.someOtherGetter // -> 'someOtherGetter' }, someOtherGetter: state => { ... } }, actions: {// In this module, // They can accept the 'root' attribute to access the root dispatch or commit someAction ({dispatch, commit, getters, rootGetters }) { getters.someGetter // -> 'foo/someGetter' rootGetters.someGetter // -> 'someGetter' dispatch('someOtherAction') // -> 'foo/someOtherAction' dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction' commit('someMutation') // -> 'foo/someMutation' commit('someMutation', null, { root: true }) // -> 'someMutation' }, someOtherAction (ctx, payload) { ... }}}}Copy the code
Register global actions in namespaced modules
To register a global action in a namespaced module, add root: true and put the action definition in a function handler. Such as:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
Copy the code
A binding function with a namespace
When using functions such as mapState, mapGetters, mapActions, and mapMutations to bind a module with a namespace, it can be cumbersome to write:
computed: { ... mapState({ a: state => state.some.nested.module.a, b: state => state.some.nested.module.b }) }, methods: { ... mapActions([ 'some/nested/module/foo', // -> this['some/nested/module/foo']() 'some/nested/module/bar' // -> this['some/nested/module/bar']() ]) }Copy the code
In this case, you can pass the module’s space name string as the first argument to the above function, so that all bindings automatically use the module as a context. Thus the above example can be simplified as:
computed: { ... mapState('some/nested/module', { a: state => state.a, b: state => state.b }) }, methods: { ... mapActions('some/nested/module', [ 'foo', // -> this.foo() 'bar' // -> this.bar() ]) }Copy the code
In addition, you can create helper functions based on a namespace by using createNamespacedHelpers. It returns an object containing the new component binding helper function bound to the given namespace value:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
Copy the code
Notes for plug-in developers
If you are developing a Plugin that provides modules and allows users to add them to the Vuex Store, you may need to consider the spatial name of the module. In this case, you can use the plugin’s parameter object to allow the user to specify the space name:
Export function createPlugin (options = {}) {return function (store) {// The space name added to the plugin module type (type) to const namespace = options. The namespace | | 'store. Dispatch (namespace +' pluginAction ')}}Copy the code
Module dynamic registration
After the store is created, you can register the module using the store.registerModule method:
Import Vuex from 'Vuex' const store = new Vuex.Store({/* options */}) // registerModule 'myModule' store.registerModule('myModule', {/ /... }) // Register nested modules 'nested /myModule 'store.registerModule(['nested', 'myModule'], {//... })Copy the code
Can be used after store. State. MyModule and store state. Nested. MyModule access module.
Module dynamic registration enables other Vue plug-ins to use Vuex to manage state by attaching new modules to the Store. For example, vuex-router-Sync (New Window) plug-in combines VUe-Router and VUex through dynamic registration module to implement application route status management.
You can also use store.unregisterModule(moduleName) to dynamically unload modules. Note that you cannot use this method to uninstall static modules (that is, modules declared when a store is created).
Note that you can check if the module has been registered with the store by using the store.hasModule(moduleName) method.
Keep the state
When registering a new Module, you will most likely want to preserve the state of the past, such as an application rendered from a server. You can archive this with the preserveState option: store.registerModule(‘a’, module, {preserveState: true}).
When you set preserveState: true, the module is registered and action, mutation, and getters are added to the store, but state is not. This assumes that the store state already contains the Module state and you don’t want to overwrite it.
Module reuse
Sometimes we may need to create multiple instances of a module, for example:
- Create multiple stores that share the same module (e.g. when
runInNewContext
Options arefalse
或'once'
In order toAvoiding stateful singletons in server-side rendering (Opens New Window)) - Register the same module multiple times in a store
If we use a pure object to declare a module’s state, the state object will be shared by reference, leading to contamination of store or module data when the state object is modified.
This is actually the same problem with data in Vue components. So the solution is the same — use a function to declare the module state (supported only in 2.3.0+) :
Const MyReusableModule = {state: () => ({foo: 'bar'}), // mutation, Action and getter, etc... }Copy the code
From https://vuex.vuejs.org/zh/Copy the code