少侠请重新来过 - Vue学习笔记(八) - Vuex

281 阅读4分钟

Vuex 是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

State

Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

Vuex 的状态存储是响应式的,我们可以通过计算属性 computedState -> 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,他可以依赖于stategetters计算,就跟组件的计算属性一样。通过 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>

触发Aciton

项目结构

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   # 产品模块

Vue-状态管理流程

Vue-状态管理流程