What is Vuex?
Vuex is a state management pattern developed specifically for Vue.js applications . It uses centralized storage to manage the status of all components of the application, and uses corresponding rules to ensure that the status changes in a predictable way.
What is the state management pattern?
Let's start with a simple Vue counting application:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{
{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
This state self-management application consists of the following parts:
- state , the data source that drives the application;
- view , declaratively maps state to views;
- actions , respond to state changes caused by user input on the view .
Here is a simple illustration representing the idea of "one-way data flow":
However, the simplicity of one-way data flow is easily broken when our application encounters multiple components sharing state :
- Multiple views depend on the same state. ==》The method of passing parameters will be very cumbersome for multi-layer nested components, and it is powerless for state transfer between sibling components.
- Actions from different views require changing the same state. ==》We often use parent-child components to directly reference or use events to change and synchronize multiple copies of the state.
By defining and isolating various concepts in state management and maintaining the independence between views and states by enforcing rules, our code will become more structured and easier to maintain. This is the basic idea behind Vuex.
When should you use Vuex?
- Shared data and behaviors need to be split;
- Complex asynchronous logic requires the integration of multiple modules for state evolution and detailed debugging information;
- Requires third-party plug-ins;
- Since it has a globally unique data source, it facilitates cross-platform implementation . Similar to NUXT, SSR uses Vuex for front-end and back-end data synchronization.
- It is necessary to comprehensively consider multiple component life cycles , sequence, and implement specific logic.
start
The core of every Vuex application is the store. A "store" is basically a container that contains most of the state in your application . There are two differences between Vuex and simple global objects:
-
Vuex's state storage is reactive. When the Vue component reads the state from the store, if the state in the store changes, the corresponding component will be updated efficiently accordingly.
-
You cannot directly change the state in the store. The only way to change the state in the store is to explicitly commit a mutation . This allows us to easily track every state change, allowing us to implement some tools to help us better understand our application.
The simplest Store
Create a store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
Get the state object through store.state
and store.commit
trigger state changes through methods:
store.commit('increment')
console.log(store.state.count) // -> 1
In order to access properties in a Vue component this.$store
, you need to provide the Vue instance with the created store. Vuex provides a store
mechanism to "inject" the store as an option from the root component to all sub-components:
new Vue({
el: '#app',
store: store,
})
Submit a change from a component's method:
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
State (single state tree)
Get Vuex state in Vue component
Since Vuex's state store is reactive, the easiest way to read state from a store instance is to return some state in a computed property:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{
{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
Whenever store.state.count
it changes, the calculated properties will be re-calculated and an update of the associated DOM will be triggered.
However, this pattern results in components relying on global state singletons. In a modular build system, state needs to be imported frequently in every component that needs to use it, and state needs to be mocked when testing components.
Vuex store
provides a mechanism to "inject" state from the root component into each child component through options (needs to be called Vue.use(Vuex)
):
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
By registering options in the root instance store
, the store instance will be injected into all sub-components under the root component, and the sub-components can be this.$store
accessed. Let's update Counter
the implementation below:
const Counter = {
template: `<div>{
{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState
Helper function
When a component needs to obtain multiple states, declaring these states as computed properties will be somewhat repetitive and redundant. To solve this problem, we can use mapState
helper functions to help us generate computed properties, allowing you to press fewer keys:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
When the name of the mapped calculated attribute is the same as the name of the child node of the state, we can also pass mapState
a string array.
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
object spread operator
mapState
The function returns an object. How can we mix this with local computed properties? Often, we need to use a utility function to merge multiple objects into one so that we can pass the final object to computed
a property. But since the object spread operator is available, we can greatly simplify the writing:
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
Getter
Sometimes we need to derive some state from the state in the store, such as filtering and counting the list:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
If multiple components need to use this property, we either need to duplicate the function, or extract a shared function and import it in multiple places - neither method is ideal.
Vuex allows us to define "getters" in the store (which can be thought of as computed properties of the store). Just like computed properties, the return value of a getter is cached according to its dependencies, and is only recomputed when its dependency values change.
Getter accepts state as its first parameter:
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)
}
}
})
Access via properties
Getters are exposed as store.getters
objects and you can access the values as properties:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getters can also accept other getters as second arguments:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
We can use it easily in any component:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
Note that getters are cached when accessed through properties as part of Vue's reactive system.
Access via method
You can also pass parameters to the getter by letting the getter return a function. Very useful when you query arrays in the store.
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
Note that when the getter is accessed through a method, it will be called every time without caching the result.
mapGetters
Helper function
mapGetters
The helper function simply maps the getters in the store to local computed properties:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
If you want to rename a getter property by another name, use the object form:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
Mutation
The only way to change the state in the Vuex store is to submit a mutation. Mutations in Vuex are very similar to events: each mutation has a string event type (type) and a callback function (handler) . This callback function is where we actually make the state changes, and it accepts state as the first parameter:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
You cannot call a mutation handler directly. This option is more like an event registration: "When a mutation of type is triggered , call this function." To wake up a mutation handler, you need to call the store.commit method increment
with the corresponding type :
store.commit('increment')
Submit payload
You can store.commit
pass in additional parameters, the payload of the mutation :
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
In most cases, the payload should be an object so that it can contain multiple fields and the recorded mutations will be more readable:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
Object style submission
Another way to submit a mutation is to use type
an object containing properties directly:
store.commit({
type: 'increment',
amount: 10
})
When using object-style submission, the entire object is passed as payload to the mutation function, so the handler remains unchanged:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Mutation needs to comply with Vue's response rules
Since the state in the Vuex store is responsive, when we change the state, the Vue component that monitors the state will automatically update. This also means that mutations in Vuex also need to follow the same precautions as using Vue:
-
It's best to initialize all required properties in your store ahead of time.
-
When you need to add new properties on an object, you should
-
use
Vue.set(obj, 'newProp', 123)
, or -
Replace old objects with new objects. For example, using the object spread operator (opens new window) we can write:
state.obj = { ...state.obj, newProp: 123 }
Use constants instead of Mutation event types
Using constants instead of mutation event types is a very common pattern in various Flux implementations. This allows tools like linters to be useful, and placing these constants in separate files allows your code collaborators to see at a glance the mutations contained in the entire app:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
It's up to you whether to use constants or not - this can be helpful in large projects where multiple people are working together. But if you don't like it, you don't have to do it.
Mutation must be a synchronous function
An important principle to remember is that mutations must be synchronous functions . Why? Please refer to the following example:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
Now imagine that we are debugging an app and observing the mutation log in devtool. Every mutation is recorded, devtools needs to capture snapshots of the previous state and the next state. However, in the above example the callback in the asynchronous function in the mutation makes this impossible: because the callback function has not yet been called when the mutation fires, devtools has no idea when the callback function is actually called - essentially Any state changes made within the callback function are not traceable.
Submit Mutation in component
You can use commit mutations in components this.$store.commit('xxx')
, or use mapMutations
helper functions to map methods in components to store.commit
calls (need to be injected at the root node store
).
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Next step: Action
Mixing asynchronous calls within mutations can make your program difficult to debug. For example, when you call two mutations that contain asynchronous callbacks to change state, how do you know when to call back and which one to call back first? This is why we distinguish between these two concepts. In Vuex, mutations are all synchronous transactions :
store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。
Action
Action is similar to mutation, except that:
- Action submits a mutation rather than directly changing the state.
- Action 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')
}
}
})
The Action function accepts a context object with the same methods and properties as the store instance, so you can call context.commit
Submit a mutation, or obtain state and getters through context.state
and . context.getters
When we introduce Modules later , you will know why the context object is not the store instance itself.
In practice, we often use ES2015 parameter destructuring (opens new window) to simplify the code (especially when we need to call it commit
many times):
actions: {
increment ({ commit }) {
commit('increment')
}
}
DistributeAction
Action store.dispatch
is triggered via methods:
store.dispatch('increment')
At first glance, it seems unnecessary. Wouldn't it be more convenient for us to distribute mutations directly? In fact, this is not the case. Remember the restriction that mutations must be executed synchronously ? Action is not restricted! We can perform asynchronous operations inside the action :
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions supports the same payload method and object method for distribution:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
Let’s look at a more practical shopping cart example that involves calling an asynchronous API and dispatching multiple mutations :
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
Note that we are performing a series of asynchronous operations and recording the side effects (ie, state changes) of the action by submitting mutations.
Distribute Actions in Components
this.$store.dispatch('xxx')
You use dispatch actions in the component , or use mapActions
auxiliary functions to map the component's methods to store.dispatch
calls (you need to inject them in the root node first store
):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
CombinationAction
Actions are usually asynchronous, so how do you know when the action ends? More importantly, how can we combine multiple actions to handle more complex asynchronous processes?
First, you need to understand store.dispatch
that you can handle the Promise returned by the handler function of the triggered action and store.dispatch
still return the Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
Now you can:
store.dispatch('actionA').then(() => {
// ...
})
It is also possible in another action:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
Finally, if we utilize async / await (opens new window) , we can combine actions as follows:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
One
store.dispatch
action function 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
Due to the use of a single state tree, all the state of the application will be concentrated into a relatively large object. When an application becomes very complex, store objects have the potential to become quite bloated.
In order to solve the above problems, Vuex allows us to split the store into modules . Each module has its own state, mutation, action, getter, and even nested submodules - divided in 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 的状态
store.state.b // -> moduleB 的状态
Module's local state
For mutations and getters inside a module, the first parameter received is the module’s local state object .
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
Similarly, for actions inside the module, the local state is context.state
exposed, and the root node state is context.rootState
:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
For getters inside the module, the root node status will be exposed as the third parameter:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
Namespaces
By default, actions, mutations and getters inside a module are registered in the global namespace - this allows multiple modules to respond to the same mutation or action.
If you want your module to have a higher degree of encapsulation and reusability, you can namespaced: true
make it a module with a namespace by adding it. When a module is registered, all its getters, actions, and mutations will automatically be named according to the module's registered path. For example:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
Namespace-enabled getters and actions receive localized getter
, dispatch
and commit
. In other words, you do not need to add additional space name prefixes within the same module when using module assets. namespaced
There is no need to modify the code within the module after changing the properties.
Access global content (Global Assets) within a namespace module
If you wish to use global state and getters, rootState
and rootGetters
will be passed into the getter as the third and fourth parameters, and context
the action will also be passed through the object's properties.
If you need to distribute actions or submit mutations in the global namespace, pass it to or { root: true }
as the third parameter .dispatch
commit
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 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) { ... }
}
}
}
Register a global action in a namespace module
If you need to register a global action in a module with a namespace, you can add it root: true
and place the definition of this action handler
in a function. For example:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
Bind function with namespace
When using mapState
, mapGetters
, mapActions
and mapMutations
these functions to bind namespace modules, it can be tedious 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']()
])
}
For this case, you can pass the module's spatial name string as the first argument to the above function, so that all bindings will automatically have that module as context. So the above example can be simplified to:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
Furthermore, you can createNamespacedHelpers
create helper functions based on a certain namespace by using. It returns an object with 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'
])
}
}
Notes to plugin developers
If you develop a plug-in that provides modules and allows users to add them to the Vuex store, you may need to consider the space name of the module. For this case, you can allow the user to specify the space name via the plugin's parameter object:
// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin (options = {}) {
return function (store) {
// 把空间名字添加到插件模块的类型(type)中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
Module dynamic registration
After the store is created , you can store.registerModule
register the module using the method:
import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
You can then access the module's status via store.state.myModule
and .store.state.nested.myModule
The module dynamic registration function allows other Vue plug-ins to use Vuex to manage state by attaching new modules to the store. For example, the vuex-router-sync (opens new window) plug-in combines vue-router and vuex through a dynamic registration module to implement application routing status management.
You can also use store.unregisterModule(moduleName)
to dynamically unload modules. Note that you cannot use this method to unload static modules (that is, modules declared when creating the store).
Note that you can use store.hasModule(moduleName)
the method to check whether the module has been registered to the store.
retain state
When registering a new module, you will most likely want to preserve past state, for example from a server-rendered application. You can preserveState
archive it via option: store.registerModule('a', module, { preserveState: true })
.
When you set it up preserveState: true
, the module is registered and actions, mutations, and getters are added to the store, but state is not. This assumes that the store's state already contains the module's 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. to avoid stateful singletons in server-side rendering when the option is or (opens new
runInNewContext
windowfalse
) )'once'
- Register the same module multiple times in a store
If we use a pure object to declare the state of the module, then the state object will be shared through references, leading to the problem of data contamination between stores or modules when the state object is modified.
data
In fact this is the same problem as in Vue components . So the solution is the same - use a function to declare the module state (only supported in 2.3.0+):
const MyReusableModule = {
state: () => ({
foo: 'bar'
}),
// mutation, action 和 getter 等等...
}