What 什么是Vuex
官方的定义是这样的:1
Vuex is a state management pattern + library for Vue.js applications.
显然它是Vue官方为Vue.js提供的库,功能是数据状态管理。它是在一个应用的层级对所有组件做集中状态管理的工具。来看一下Vuex在应用中的结构:
定义中提到了state management pattern,即状态管理模式,那问题就来了为什么需要这种设计模式呢,普通的模式什么样呢,多加一层有什么好处呢,软件开发中有一条不成文的规律:多加一层。难度就下降一倍。我们来看看这样的设计是否达到了这样的效果。
我们来看一下正常情况下的模式:
即所谓的State–View–Action模式:
这是一个简化的自包含系统,遵循单向数据流的核心原则。
View:用于接收用户输入
Actions:响应用户输入来改变状态
State:接收用户数据,并把数据传递到View层来显示
然而当多个View共享一个State时,这种平衡被破坏了。
- 多个view可能依赖相同的state
- Actions来自不同的View需要改变相同的state
对于问题一, 如果是层级嵌套的组件View的状态更新我们可以通过props来传递,但是层级过多的时候很麻烦而同级组件不起作用。
对于问题二,我们经常需要查询父子空间的应用重新存储State,或通过事件来更改和同步多个使用状态的地方,会导致代码脆弱,不可维护。
基于以上原因,我们抽象出了图一所谓的状态管理模式,即:把共享的状态取出来,变成一个全局单例。无论是访问状态还是追踪改变,都通过这个单例进行。
这就是Vuex背后的核心思想,通过定义和分离状态管理的概念,使views和states保持了独立,从而给了我们更多的代码结构和维护性。
Vuex是专门为Vue.js定制开发的,利用的Vue.js的响应式系统和高效更新的优势,是Vue项目下状态管理插件的首选。
Why 为什么要使用Vuex
其实有时候知道了what,我们也就知道了why。自己定义一个全局的单例貌似也可以实现同样的效果,我们来具体说说为什么Vuex和全局单例是不同的:
- Vuex作为应用的存储中心,它的数据是响应式的。如果状态发生改变,界面会进行高效自动更新。
- 你并不能够直接改变存储的状态,我们其实是通过committing mutations的声明来改变状态的,这使得我们的改变是可以被如devtools工具追踪到,有助于bug调试。
When 何时使用
Vuex虽然帮我们共享了状态管理,但也额外引入了更多的概念和样板文件。如果你的应用比较简单,你就用store pattern,一种追踪变化和实时响应的存储模式。如果你是中大型项目,尤其是跨层级的组件间共享状态你自然可以尝试Vuex。
How 如何使用
- 安装
npm install vuex --save
引用
1
2import Vuex from 'vuex'
Vue.use(Vuex)创建一个存储实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32const store = new Vuex.Store({
state: {
count: 0,
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false}
]
},
mutations: {
increment (state, n) {
state.count += n
}
},
actions: {
increment({commit}) {
setTimeout(() => {
commit('increment', 2)
},3000)
}
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
},
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})
大家看到上面声明了一些对象关键字如state
、mutations
、getters
,先别急,后面我们一个一个讲清楚。
挂载到Vue实例下面
1
2
3
4
5// main.js 入口文件
new Vue({
store,
render: h => h(App),
}).$mount('#app')在其他组件中使用
1 | // App.vue |
核心概念
State
State
是一个全局唯一的状态树
如何在Vue组件中获取Vuex的状态
方法一:由于state的存储是响应式的,所以我们可以在合适的计算属性里使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const store = new Vuex.Store({
state: {
count: 1
}
})
const Counter = {
template: `<div> {{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
var vm = new Vue({
el:'#example',
components: {Counter},
template:`<counter></counter>`
})
进一步优化
这样每当在其他组件使用的时候,我们都需要导入store,Vue为我们提供了一个store操作符,可以全局注入所有的子组件。1
2
3
4
5
6
7
8
9
10
11
12
13
14const Counter = {
template: `<div> {{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
var vm = new Vue({
el:'#example',
store,
components: {Counter},
template:`<counter></counter>`
})
在创建Vue根实例的时候声明store
,而后使用this.$store
的方式访问。
注意:
我这里是通过<script>
标签对导入的Vuex,如果你是通过包管理工具下载而后通过import命令导入的包,还需要声明Vue.use(Vuex)
mapState 助手
在组件中访问多个属性或getters的时候,它可以帮我们简化写法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<script>
import { mapState } from 'vuex'
export default {
name: 'HelloWorld',
data () {
return {
localCount: 5
}
},
computed: mapState({
// 可以使用箭头函数访问
count: state => state.count,
// 给count起一个别名
countAlias: 'count',
// 当使用本地数据的时候必须使用函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
</script>
在被映射的计算属性和state的子树名称相同时,可以传递一个字符串数组:1
2
3computed: mapState([
'count'
])
对象拓展运算符1
2
3
4
5
6computed: {
localComputed () { return 1 },
...mapState({
count: state => state.count
})
}
可以将mapState返回的对象结合其他的本地属性使用。
Getters
有时候我们需要基于state做一些计算,例如从一个代办事项里过滤已经做完的事情:1
2
3
4
5computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果多个地方用到这个功能,我们需要写多个函数或取出封装为一个公共组件在其他函数里导入,都不是好主意。
getters相当于Vue组件的计算属性,可以缓存它的依赖数据,当依赖改变的时候才会重新计算:1
2
3
4
5
6
7
8
9
10
11
12
13
14const store = new Vuex.Store({
state: {
count: 0,
todos:[
{id: 1, text:'...', done:true},
{id: 2, text:'...', done: false}
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
怎么访问Getters
- 属性方式访问
getters通过store.getters
返回对象,所以我们可以直接在组件中使用:1
2
3
4
5computed: {
doneTodos() {
return this.$store.getters.doneTodos // [ { "id": 1, "text": "...", "done": true } ]
}
}
getters可以接收其他的getters作为第二个参数:1
2
3
4
5
6
7
8getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length // 1
}
}
- 方法方式访问
我们可以返回一个函数,这在查询数组的时候尤其有用1
2
3getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
1 | this.$store.getters.getTodoById(2) // { "id": 2, "text": "...", "done": false } |
注意:
通过方法调用没办法缓存,每一次都会重新计算
mapGetters助手
它负责把store getters映射到本地属性,所以使用时代码我们可以简化如下:1
2
3
4
5
6computed: {
...mapGetters([
'doneTodos',
'doneTodosCount'
])
}
可以用以下方式起一个别名:1
2
3...mapGetters({
doneCount: 'doneTodosCount'
})
Mutations
mutations其实是Vuex里面改变state的唯一方式,也就是说你必须通过提交一个mutation来改变state的状态,不能直接操作state。它和事件比较像,有一个字符串的类型和一个处理器,这个处理器函数来执行实际state的改变。它接收state作为第一个参数。1
2
3
4
5
6
7
8
9
10const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count ++
}
}
})
需要注意的是你不能直接调用它,它更像是一个事情注册,当类型为increment
的mutation函数被触发时,调用这个处理器。为了调用这个处理器,我们需要使用store.commit('increment')
。1
2
3
4
5<template>
<div>
<button @click="$store.commit('increment')">++</button> // 1
</div>
</template>
使用Payload
你可以传递给store.commit
一个额外的参数,payload将会被调用。1
2
3
4
5mutations: {
increment (state, n) {
state.count += n
}
}
1 | <button @click="$store.commit('increment',2)">++</button> |
一般payload可能包含多个字段,这样的话我们可以更具体的访问:1
2
3
4
5mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
1 | <button @click="$store.commit('increment',{amount: 2})">++</button> |
对象方式提交
我们可以采用对象方式提交:1
store.commit({type:'increment', amount: 2})
此时,整个对象会被传递给payload,所以,mutation的处理方式是相同的:1
2
3
4
5mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
对于mutation的类型使用常量
对于mutation的类型我们可以使用常量,这样的话可以使用语法高亮,不容易拼写错误,而且把他们单独放在一个文件中,对于团队开发会比较方便,直接能在一个文件里面看到所有的mutation,这是一个常见的使用惯例。1
2// mution-type.js
export const INCREMENT = 'increment'
1 | import { INCREMENT } from './mutaion-types' |
Mutations必须是同步的
由于devtools
里面调试时需要抓取state改变前后的对比数据,但是异步函数不知道什么时候调用完成,所以没有办法追踪。
在组件中使用Mutations
我们可以使用this.$store.commit(‘xxxx’)来正常提交,也可以使用mapMutations
助手(必须把store注入根节点)做映射:1
2
3
4
5mutations: {
[INCREMENT] (state,payload) {
state.count += payload.amount
}
}
1 | // HTML |
还可以设置别名:1
2
3
4
5
6
7
8// HTML
<button @click="add({amount: 3})">++</button>
// JS
methods: {
...mapMutations({
add: 'increment'
})
}
Actions
为异步操作而生
- 提交操作给mutations而不是直接改变state
- 可以执行任何异步操作
先看一段声明:1
2
3
4
5actions: {
increment (context) {
context.commit('increment')
}
}
context
包括了所有store上下文的所有方法和属性,所以他可以访问context.commit
,context.getters
,context.state
。
在实践中我们使用ES2015的参数解构来简化我们的代码,尤其是需要调用commit多次时:1
2
3
4
5actions: {
increment ({commit}) {
commit('increment')
}
}
发送Actions
我们使用store.dispatch
方法来触发Actions对象。1
store.dispatch('increment')
之所以搞的这么麻烦是因为Actions支持异步操作:1
2
3
4
5incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
它在使用的时候和Mutations支持相同的payload和对象格式:1
2
3
4
5// payload格式
<button @click="$store.dispatch('increment', {amount: 3})">++</button>
// 对象格式
<button @click="$store.dispatch({type: 'increment', amount: 3})">++</button>
1 | actions: { |
它在组件中的用法和Mutations相同,不在赘述。1
2
3
4
5
6
7// JS
import { mapActions } from 'vuex'
methods: {
...mapActions({
add: 'increment'
})
}
组合Actions
Actions既然是异步的,那么我们怎么知道它什么时候执行完呢,Actions的处理器可以接收返回一个Promise:1
2
3
4
5
6
7
8actionA({commit}, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment', payload)
resolve('finish')
}, 1000)
})
}
所以现在可以这样接收:1
2
3this.$store.dispatch({type: 'actionA', amount: 3}).then((res)=> {
console.log(res) // finish
})
我们还可以在另一个action里嵌套:1
2
3
4
5actionB({dispatch,commit},payload) {
return dispatch('actionA',payload).then(() => {
commit('incrementOne')
})
}
1 | this.$store.dispatch('actionB',{amount: 4}) |
当然,我们可以使用async/wait
组合的方式:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17incrementValue({state, commit}, payload) {
return new Promise((resolve, reject) => {
// setTimeout(() => {
commit('increment', payload)
resolve('finish')
}, 1000)
},
async actionA({dispatch, commit}, payload) {
await dispatch('incrementValue', payload)
},
async actionB({dispatch,commit},payload) {
await dispatch('actionA',payload)
setTimeout(() => {
commit('incrementOne')
},1000)
}
1 | this.$store.dispatch('actionB',{amount: 4}) |
有时候我们action处理器来自不同的模块,我们需要等到所有的action处理完毕后返回一个Promise。
模块
为了防止store在一个文件中越来越大,Vuex支持模块划分:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import ModuleA from './moduleA'
import ModuleB from './moduleB'
const store = new Vuex.Store({
state: {
count: 0,
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false}
]
},
modules: {
a:ModuleA,
b:ModuleB
}
})
1 | // ModuleA.js |
1 |
|
在模块内部,mutations和getters里的参数是接收文件内自己的状态,actions的context.state也是一样的,如果我们要访问根节点上的状态,我们需要通过context.rootState
导出:1
2
3
4
5
6
7
8actions: {
increment({ state , commit, rootState}) {
console.log(rootState.count)
if (rootState.count === 0) {
commit('increment')
}
}
}
默认情况下,模块内外的事件都是被注册到全局命名空间的,所以访问actions/mutaions的时候,模块间的相同事件是同时响应的,如果想要自包含,需要添加namespaced: true,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// ModulesA.js
export default {
namespaced: true,
state: {
count:10
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
someAction ({state, commit,rootState,rootGetters}) {
dispatch('someOtherAction') // moduleA/someOtherAction
dispatch('someOtherAction', null, {root: true}) // someOtherAction
},
someOtherActon(ctx, payload) {
// ...
}
}
访问的时候加上路径就可以:1
<button @click="$store.dispatch('a/increment')">moduleA++</button>
模块间也是可以继承和嵌套的,具体看:vuex-modlues。
在命名空间下访问全局资源
如果想使用全局的state和getters,那么rootState
和rootGetters
作为getter函数的第三个和四个参数,他们也被封装到context上下文传递给 aciton 函数。
如果想要dispatch
或commit
到全局命名空间,需要添加{root: true}
为第三个参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16getters: {
someGetter(state, getters, rootState, rootGetters){
getters.someOtherGetter // mudlueA/someOtherGetter
rootGetters.someOtherGetter // someOtherGetter
},
someOtherGetter: state => { //... }
},
actions: {
someAction ({state, commit,rootState,rootGetters}) {
dispatch('someOtherAction') // moduleA/someOtherAction
dispatch('someOtherAction', null, {root: true}) // someOtherAction
},
someOtherActon(ctx, payload) {
// ...
},
}
在命名空间下注册全局资源
在actions对象内,声明action名称对象,在该对象内使用root:true
,并把原 action 内的事件处理给到函数handler
。1
2
3
4
5
6
7
8
9
10// ModuleA.js
actions: {
add:{
root: true,
handler ({state, commit}, payload) {
commit('increment') // moduleA/increment
commit ('increment', payload, {root: true}) // increment
}
}
}
命名空间如何绑定助手
通常情况下,这样来写(以state为例):1
2
3
4
5
6import { mapState } from 'vuex'
computed: {
...mapState({
namespacedCount: state => state.a.count
}),
}
如果mapState里面的对象比较多,写起来有点啰嗦,所以我们可以把命名空间模块写为mapState的第一个参数:1
2
3
4
5computed: {
...mapState('a',{
namespacedCount: state => state.count
}),
}
如果mapState,mapGetters,mapMutions都用到这个命名空间,我们没有必要在每一个里面都写一个空间命名参数,可以使用createNamespacedHelpers做进一步简化:1
2
3
4
5
6
7import { createNamespacedHelpers } from 'vuex';
const { mapState } = createNamespacedHelpers('a')
computed: {
...mapState({
namespacedCount: state => state.count
}),
}
总结
What | 首先我们介绍了什么是Vuex,它是Vue官方提供的状态管理工具,利用了Vue的响应式特性。
Why | 进而我们指出了为什么要使用Vuex,基于它的响应式更新机制及辅助调试。
When | 我们也介绍了你应该什么时候使用Vuex,如何权衡。
How | 我们花了90%以上的篇幅介绍的Vuex的使用细节:
- 如何安装
- 如何声明
- 如何访问
接着我们介绍了Vuex的六大核心概念,他们分别是:
- State
全局状态树,类比组件中的data
属性。 - Getters
类比组件中的computed
属性,可以缓存数据,用于计算逻辑。 - Mutations
操作state的唯一方式,类比组件中的method。 - Actions
做异步事件处理,只能提交给mutations,不能直接操作state,同样类比组件中的method。 - Mudules
用于复杂场景下的模块划分,可以理解为封装。
参考:
Vuex