Vuex 是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
State
Vuex
使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
Vuex
的状态存储是响应式的,我们可以通过计算属性 computed
,State -> Computed -> 触发dom更新
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state:{
msgList:[]
}
})
new Vue({
store,
//...
computed:{
msgList:function(){
return this.$store.state.msgList
}
}
})
mapState 辅助函数
当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键
const store = new Vuex.Store({
state:{
count:0
}
})
<template>
<div>
<p>{{localCount}}</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data(){
return{
countStr:'count:'
}
},
computed:mapState({
count: 'count',
localCount(state){
return this.countStr + state.count
}
})
}
</script>
Getter
Vuex
允许我们在 store
中定义getter
(可以认为是 store
的计算属性)。就像计算属性一样,getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter
可以接收第一个参数是 state
, 第二个参数是 getters
,他可以依赖于state
跟getters
计算,就跟组件的计算属性一样。通过 store.getters
访问。
可以让getter
返回一个函数,实现对getter
传参。
mapGetters 辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
const store = new Vuex.Store({
state:{
id:0,
count:0
},
getters:{
countStr:state=>'getCountStr' + state.count,
getValue:(state,getters) => value => getters.countStr + state.id + value
},
})
<template>
<div>
<input type="text" name="" v-model="value" id="">
<p>{{countStr}}</p>
<p>{{localGetCount}}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data(){
return{
value:'',
localCountStr:'count:'
}
},
computed:{
...mapGetters([
'countStr'
]),
localGetCount(){
return this.$store.getters.getValue(this.value)
},
}
}
</script>
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,Vuex 中的 mutation 类似于事件,每一个 mutation 都有一个字符串的事件类型,和一个回调函数,回调函数中接收的第一个参数是 state 。
mutation 的函数必须是一个同步函数
const store = new Vuex.Store({
state:{
msgList:[],
id:0,
count:0
},
mutations:{
insertMsg(state,payload){
var item = {
id:state.id++,
msg:payload.msg
}
state.msgList.push(item)
state.count++
}
}
})
我们不能直接调用一个 mutation handle ,我们可以通过 store.commit('insertMsg')
触发事件
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象
this.$store.commit({
type:'insertMsg',
msg:this.value
})
Mutation 需遵守 Vue 的响应规则
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
- 使用
Vue.set(obj, 'newProp', 123),
或者 - 新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
- 使用
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
分发 Action
store.dispatch('increment')
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
组件中分发 Action
你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise
const store = new Vuex.Store({
state:{
msgList:[],
id:0,
count:0
},
mutations:{
insertMsg(state,payload){
var item = {
id:state.id++,
msg:payload.msg
}
state.msgList.push(item)
state.count++
},
},
actions:{
insertMsg({commit},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.5){
commit('insertMsg',payload);
resolve();
}else{
reject('插入失败')
}
}, 1000);
})
}
}
})
<!--组件调用-->
<template>
<div>
<input type="text" name="" v-model="value" id="">
<button @click="handleInsert" >submit</button>
<p v-for="(item,index) in msgList" :key='index'>
{{item.id}}:{{item.msg}}
<button @click='handleRemove(item.id)'>del</button>
</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapGetters } from 'vuex'
import { mapMutations } from 'vuex';
export default {
data(){
return{
value:'',
localCountStr:'count:'
}
},
computed:{
...mapState({
msgList: state => state.msgList,
count: 'count',
localCount(state){
return this.localCountStr + state.count
}
}),
},
methods:{
handleInsert(payload){
if(this.value !== ''){
this.$store.dispatch({
type:'insertMsg',
msg:this.value
}).then(()=>{
//成功回调
console.log('suss')
this.value = '';
},(error)=>{
//失败回调
alert(error)
})
}
},
}
}
</script>
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 的状态
命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
const child = {
namespaced: true,
state:{
count:0
},
getters:{
logCount:state=>'child-log-count:'+state.count
},
mutations:{
addCount(state,payload){
let offset = payload.count - 0 || 1
state.count += offset
},
reduceCount(state,payload){
let offset = payload.count - 0 || 1
state.count -= offset
},
logCount(state){
console.log(`child-mutations-log-${state.count}`)
}
},
actions:{
add({commit,dispatch,getters,rootState},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.3){
commit('addCount',payload);
dispatch('add',payload,{ root: true });
commit('logCount');
resolve();
}else{
commit('logCount');
reject('添加失败')
}
}, 300);
})
},
reduce({commit,dispatch,getters,rootState},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.3){
commit('reduceCount',payload);
dispatch('reduce',payload,{ root: true });
commit('logCount');
resolve();
}else{
reject('添加失败')
}
}, 1000);
})
}
},
modules:{
grandchild:{
namespaced: true,
state:{
count:0
},
getters:{
logCount:state=>'grandchild-log-count:'+state.count
},
mutations:{
addCount(state,payload){
let offset = payload.count - 0 || 1
state.count += offset
},
reduceCount(state,payload){
let offset = payload.count - 0 || 1
state.count -= offset
},
logCount(state){
console.log(`grandchild-mutations-log-${state.count}`)
}
},
actions:{
add({commit,dispatch,getters,rootState},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.3){
commit('addCount',payload);
dispatch('child/add',payload,{ root: true });
commit('logCount');
resolve();
}else{
commit('logCount');
reject('添加失败')
}
}, 300);
})
},
reduce({commit,dispatch,getters,rootState},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.3){
commit('reduceCount',payload);
dispatch('child/reduce',payload,{ root: true });
commit('logCount');
resolve();
}else{
reject('添加失败')
}
}, 1000);
})
}
},
}
}
}
const store = new Vuex.Store({
namespaced: true,
state:{
count:0
},
getters:{
logCount:state=>'parent-log-count:'+state.count
},
mutations:{
addCount(state,payload){
let offset = payload.count - 0 || 1
state.count += offset
},
reduceCount(state,payload){
let offset = payload.count - 0 || 1
state.count -= offset
},
logCount(state){
console.log(`parent-mutations-log-${state.count}`)
}
},
actions:{
add({commit,dispatch,getters,rootState},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.3){
commit('addCount',payload);
commit('logCount');
resolve();
}else{
reject('添加失败')
}
}, 1000);
})
},
reduce({commit},payload){
return new Promise((resolve,reject)=>{
setTimeout(() => {
if(Math.random() > 0.5){
commit('reduceCount',payload);
commit('logCount');
resolve();
}else{
commit('logCount');
reject('添加失败')
}
}, 1000);
})
}
},
modules:{
child
}
})
<template>
<div>
<p>demo9</p>
<input type="number" name="" v-model="value" id="">
<button @click='add'>add</button>
<button @click='reduce'>reduce</button>
<p>grandchildCount : {{grandchildCount}}</p>
<p>{{grandchildLog}}</p>
<p>childCount : {{childCount}}</p>
<p>{{childLog}}</p>
<p>parentCount : {{parentCount}}</p>
<p>{{parentLog}}</p>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data(){
return{
value:1
}
},
computed:{
...mapState({
grandchildCount:state=>{
return state.child.grandchild.count
},
childCount:state=>{
return state.child.count
},
parentCount:state=>{
return state.count
}
}),
...mapGetters({
grandchildLog:'child/grandchild/logCount',
childLog:'child/logCount',
parentLog:'logCount'
})
},
methods:{
add(){
this.$store.dispatch('child/grandchild/add',{count:this.value}).then(()=>{
console.log('添加成功')
},()=>{
console.log('添加失败')
})
},
reduce(){
this.$store.dispatch('child/grandchild/reduce',{count:this.value}).then(()=>{
console.log('减少成功')
},()=>{
console.log('减少失败')
})
},
}
}
</script>
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。 只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块