Vuex的五个核心概念—state/getter/mutation/action/module
1. State
Vuex 使用单一状态树——用一个对象包含全部的应用层级状态。至此它就是“唯一数据源 (SSOT)”。这也意味着,每个应用将仅仅包含一个 store 实例。
组件中获得 Vuex 状态
-
计算属性
import store from './store' const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
然而,组件可能需要频繁导入store。
-
store选项
//父组件 import store from './store' import Counter from './Counter' new Vue({ el: '#app', store, //将store从根组件“注入”到每一个子组件中 render:c=>c(Counter), })
//子组件Counter.vue能通过 `this.$store` 访问: <template> <div>{{count}}</div> </template> <script> export default { computed: { count () { return this.$store.state.count } } } </script>
辅助函数mapState
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性:
//store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex)
export default new Vuex.Store({
state:{
count:4
}
})
//Counter.vue
<template>
<div>{{sumCount}}</div>
</template>
<script>
import store from './store'
import {mapState} from 'vuex' //引入辅助函数
export default {
data(){
return{
localCount:7
}
},
computed:mapState({
count:state=>state.count, //返回count,等同于count () {return this.$store.state.count}
countAlias:'count', //count的别名是countAlias,这时候countAlias等同于count(注意单引号)
sumCount(state){ //处理多状态:store的数据加上本地的数据
return state.count+this.localCount
}
})
}
</script>
当映射的计算属性的名称与 state 的子节点名称相同时,可以给 mapState
传一个字符串数组:
computed: mapState([
'count' //等同于count:state=>state.count,
])
2. Getter
前言
假设我们需要处理store中state,例如对列表进行过滤并计数
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
然而如果很多组件都需要用到这个计算属性,那么我们只能复制这个函数,或者频繁导入它。
Vuex 允许我们在 store 中定义“getter”,就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
应用
1. getter的参数
-
getter
接受state
作为参数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) } } })
-
getter
接受第二个参数getter
getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } doneTodosCount: (state, getters) => { return getters.doneTodos.length //通过getter访问到doneTodos属性 } }
2. 通过属性访问
//访问
store.getters.doneTodos
store.getters.doneTodosCount
//组件中使用
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
3. 通过方法访问
让 getter 返回一个函数,来实现给 getter 传参。这对在 store 里的数组进行查询时非常有用
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
//访问
store.getters.getTodoById(2) //返回元素id与传过去的id相等的元素
辅助函数mapGetters
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符,将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
3. Mutation
简单示例
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。 mutation 类似于事件:每个 mutation 都有一个 事件类型 (type) 和 一个 回调函数 (handler)。它接受 state
作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
//事件类型:increment;回调函数为后面部分;
increment (state) {
state.count++
}
}
})
想要调用该事件类型的回调,需要使用store.commit()
,并传入字符串的事件类型
store.commit('increment')
提交载荷(Payload)
可以向 store.commit
传入额外的参数,这些额外参数就是 mutation 的 载荷(payload)
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样更易读:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
提交方式
1. 没有载荷
store.commit('事件类型')
2. 有载荷:
//一般写法
store.commit('increment', 10)
//载荷为一个对象时
store.commit('事件类型', {
amount: 10
})
//载荷为一个对象时-对象风格写法
store.commit({
type: '事件类型',
amount: 10
})
3. 组件中提交
this.$store.commit('事件类型')
mapMutations辅助函数
methods:{
//...
...mapMutations([
'increment', //相当于this.$store.commit('increment')
'incrementBy', //相当于`this.$store.commit('incrementBy', n)`
'incrementBy2' //相当于this.$store.commit('incrementBy2',{amount})
]),
...mapMutations({
add:'increment' //此时this.add相当于$store.commit('increment')
})
}
注意:Mutation 必须是同步函数
使用常量代替Mutation类型
// 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
}
}
})
4. Action
前言
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
简单示例
//一个往数组添加随机数的实例:
const store = new Vuex.Store({
state: {
msg: []
},
mutations: {
addMessage(state,msg){
state.msg.push(msg)
}
},
//一个异步操作:
actions: {
getMessages(context){
fetch('https://www.easy-mock.com/mock/5f4a32907e1a7f3146e313e7/api/getnum')
.then(res=>res.json())
.then(data=>{
context.commit('addMessage',data.data.number)
})
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以在这个对象中获取 state 和 getters或提交一个 mutation。
ES6中可以通过参数解构简化代码
actions: {
getMessages({commit}){
//...
commit('addMessage',data.data.number)
//...
}
}
分发Action
1. 普通分发
Action 通过 store.dispatch
方法触发,
store.dispatch('getMessages')
2. 载荷分发
// 普通
store.dispatch('事件类型', {
amount: 1
})
// 对象形式
store.dispatch({
type: '事件类型',
amount: 1
})
3. 组件中分发
this.$store.dispatch('事件类型')
mapMutations辅助函数
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'getMessages', // 相当于`this.$store.dispatch('getMessages')`
]),
...mapActions({
addmsg: 'getMessages' //this.addmsg等同于`this.$store.dispatch('getMessages')`
})
}
}
组合Action
如何知道 action 什么时候结束呢?如何才能组合多个 action,以处理更加复杂的异步流程呢?首先要知道:
store.dispatch
可以处理被触发的 action 的处理函数返回的 Promisestore.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在可以指定回调:
store.dispatch('actionA').then(() => {
// ...
})
也可以 另一个action指定回调:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后利用 async / await,我们可以如下组合 action:
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
5. Module
前言
当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块:
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 的状态
模块的局部状态
const moduleA = {
state: () => ({
count: 0
}),
//1. 对于mutation,接收的第一个参数是模块的局部状态对象。
mutations: {
increment (state) {
state.count++
}
},
//2. 对于getter,第一个参数暴露局部状态;第三个参数`rootState`暴露根节点状态
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
//3. 对于action,局部状态通过 context.state 暴露出来,根节点通过 context.rootState:
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以在模块对象中加入 namespaced: true
使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
state: () => ({ ... }),
getters: {isAdmin () { ... }},// 通过 store.getters['account/isAdmin']访问
actions: {login () { ... }},// 通过 store.dispatch('account/login')分发
mutations: {login () { ... }},//通过store.commit('account/login')提交
//模块中嵌套模块-继承父模块的命名空间
modules: {
//模块1-myPage
myPage: {
state: () => ({ ... }),
getters: {profile () { ... }}//store.getters['account/profile']
},
//模块2-mposts-进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {popular () { ... }}//store.getters['account/posts/popular']
}
}
}
}
})
在带命名空间的模块内访问全局内容
rootState
和 rootGetters
会作为第三和第四参数传入 getter,也会通过 context
对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true }
作为第三参数传给 dispatch
或 commit
即可。
modules: {
foo: {
namespaced: true,
getters: {
someOtherGetter: state => { ... }
//1. 对于getter,使用第四个参数 `rootGetters`访问根getter
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 局部的
rootGetters.someOtherGetter // -> 全局的
},
},
actions: {
someOtherAction (ctx, payload) { ... }
//2. 对于actions,接受 root 属性来访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
dispatch('someOtherAction') // -> 局部的分发
dispatch('someOtherAction', null, { root: true }) // -> 全局的分发
commit('someMutation') // -> 局部的提交
commit('someMutation', null, { root: true }) // -> 全局的提交
},
}
}
}
在带命名空间的模块注册全局 action
在action中添加 root: true
,并将这个 action 的定义放在函数 handler
中。例如:
{
//...
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 定义someAction action
}
}
}
}
}
带命名空间的绑定函数
当使用 mapState
, mapGetters
, mapActions
和 mapMutations
这些函数来绑定带命名空间的模块时:
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']()
])
}
你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文,简化后:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
简化的另一个方法:使用 createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')//基于某个命名
export default {
computed: {
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions([
'foo',
'bar'
])
}
}