首页 编程 正文

一篇文章带你吃透Vuex3的状态管理_vue.js_

2023-05-24 23:56:21 29

一. Vuex是什么

了解过Vuex的都知道,它是Vue官方的状态管理方案;可以对父子,祖孙以及兄弟组件之间进行通信;

除了使用Vuex,我们都知道还有一个方案能满足任何组件的之间的通信,那就是vue全局事件总线($ bus)

在数据接收组件内利用$ on绑定一个事件(eventName),第二个参数绑定一个回调函数来接受数据。

//接收数据的组件
this.$bus.$on('eventName',(value)=>{
   console.log(value) //{a:1,b:2}
})

在数据发送组件内利用$ emit 提交给绑定的事件(eventName),后面的参数为要传递的数据;

//发送数据的组件
var obj = {a:1,b:2}
this.$emit('eventName',obj)

那既然Vue全局事件总线($ bus)能够满足任何组件的之间的通信,为什么vue官方还要再创造出Vuex呢?

Vue全局事件总线

我们来看下面这个场景(利用事件总线进行数据共享)图片来源于尚硅谷

上图可以看到,A组件中data中有一个X属性,B,C,D组件也都需要,共享属性可以利用$ on 去获取到X属性;这样看起来,感觉不是很简单吗,没有什么啊,别急,这只是读取属性,那如果B,C,D需要修改呢?

其实只需要在B,C,D组件内去利用$ emit事件把修改的值发送给A组件,A组件再通过$ on去接受然后对X属性进行修改,光看文字是不是感觉已经很绕啦,也就是下图所示:图片来源于尚硅谷

红色箭头是B,C,D组件读取到共享属性X,绿色箭头是B,C,D组件修改X属性;

目前场景只是展示四个组件都需要一个共享属性X,通过读写,看上去都已经很乱啦,那如果大项目中有十几个,几十个组件都需要一个共享属性X呢,岂不是更乱;

Vuex状态管理

那如果要用Vuex实现X属性的共享呢?看下图:图片来源于尚硅谷

Vuex是独立于任何组件的一个存在,把A,B,C,D组件需要共享的属性放到Vuex中去管理,不需要共享的属性还是写在原组件内;此时A,B,C,D组件和Vuex是双向箭头,就是组件既可以通过其内置的api去读,也可以去修改,某一个组件修改共享的属性,其他组件获取的都是最新修改的数据;

何时使用Vuex

Vue事件总线其实也很方便,但是适合使用小项目中;对于大项目Vuex作为共享状态集中式管理,是最好的选择,方便使用以及维护;

那疑问来了,我也不清楚项目的大小怎么办,什么时候适合使用Vuex呢?

  • 项目中多个组件都需要使用或修改共同一个状态(多个组件共享同一个状态)

二. 纯vue组件案例

本来打算直接介绍引入Vuex的代码步骤和方法,但是为了更好的理解好对比,我先把我写的两个组件案例demo和代码给大家看一下,稍后再给大家看引入Vuex后的代码,虽然功能都一模一样,主要是对比Vuex使用前后的组件内部代码不同;

计算总数案例

导航二的计算总数案例组件:

代码如下:



添加人员案例

导航三添加人员案例组件:

代码如下:



此时两个组件是完全独立的,没有实现数据共享,所以在用到对方组件内数据的地方以 ”???“ 标记;

接下来,我们就开始一步一步使用Vuex去实现两个组件的数据共享;

三. Vuex工作原理和流程

下面是官方给的Vuex的工作原理图,如下:

如果想要很熟悉的使用Vuex,那我们就应该先了解其工作原理,明白了它内部的运转机制,那么我们就可以告别死记,就可以很熟悉流畅的编写代码;

从上图工作原理图来看Vuex中有三个最重要的核心对象,Actions,Mutations,State,那他们三个是什么关系,怎么协助运转呢,下面我们来看一下Vuex的工作流程;

第一种工作流程

为了方便了解,我给Vuex工作原理图稍微做了一些标注,图如下:

这个红色箭头就是组件使用Vuex的工作流程,配合上图,认真理解下面文字,看一下组件是怎么修改共享数据的:

  • vue组件内调用dispacth()函数,该函数内又两个参数,第一个参数是一个方法名‘key1’,第二个参数是传入的值
  • 由于的组件调用了dispatch,就会在Actions对象中去找dispatch函数传入的一个参数‘key1’;
  • 找到key1后,就会调用对应的函数;key1函数内部又调用了commit()函数。commit()函数也有两个参数,第一个参数是一个方法名‘KEY1’,第二个参数是传入的值;
  • 由于调用了commit,就会在Mutations对象中去找commit函数传入的第一个参数‘KEY1’;
  • 在Mutations对象找到‘KEY1’对应的函数后,Vuex内部调用Mutate对状态就行修改,修改状态后对修改状态的组件进行render

第二种工作流程

现在应该了解其工作的大致流程了吧,别急,还没有完,我们继续看下图:

这个应该是最完善的工作流程图,除了刚才我们介绍的最外圈的工作流程外,其实组件也可以直接去调用commit()去修改状态;那该怎么区分和使用呢?

  • Vuex内部有规定,Actions中主要写一些复杂的业务逻辑和异步处理,Mutations中主要修改状态;
  • 如果在组件内就可以拿到需要传递的value值,内部不在需要对value进行过多的业务了逻辑处理,可以直接commit()去Mutations中调用修改状态函数
  • 如果需要传递的value值要通过接口获取,然后还要进行复杂的业务逻辑,最好放到Actions的函数去处理;
  • 这样做的好处是Devtools开发工具可以准确的监控Vuex状态的变化;

生活化的Vuex工作原理

以上就是Vuex的全部工作原理以及流程;但是为了让大家更好的去记住和理解,我又模拟了下面这个场景:

Vuex就相当于一个饭店,vue组件就是顾客,Actions就是服务员,Mutations就是厨师,state就是做出来的菜和饭;我们把修改共享状态的任务改成点菜,我们可以走一下点餐流程:

通过服务员点餐的流程:

  • 首先,顾客因为不熟悉饭店菜品点菜的时候需要询问,所以要找‘服务员1’点菜,或者是通过扫描桌上外卖平台点餐
  • 其次,‘服务员1’整理好菜单后就交给了后厨‘厨师1’去做。后厨安装的有监控,以后想要知道哪个厨师做的哪个顾客的菜都能查询到;
  • 最后,厨师1做好菜,顾客就可以直接拿到;

顾客自己点餐的流程:

  • 首先,顾客很熟悉这家饭店,菜系了解,厨师也都认识这样就很简单,直接找到‘厨师1’要了几个菜;
  • 最后,厨师1做好菜,顾客就可以直接拿到;

现在应该很熟悉Vuex的工作流程了吧,很简单,学会点菜就行,哈哈,那接下来我们就要开始使用Vuex啦;

四. 在项目中引入Vuex

安装Vuex

首先,你的电脑要安装node环境,使用npm安装:

npm install vuex --save

创建store

然后在项目src文件夹下创建一个store文件夹,在其里面创建index.js:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
  state: {},
  getters:{},
  mutations: {},
  actions:{},
  modules:{}
})
export default store

在Vue中挂载store

最后在main.js文件内引入创建的store:

import Vue from 'vue'
import App from './App'
import store from './store'

new Vue({
  el: '#app',
  components: { App },
  template: '',
  store
})

五. Vuex的核心属性用法

知道一个组件怎么使用,另一个就不在话下,这里我以计算总数组件为例;

在创建store实例的时候,大家可以看到Vuex内部有五个核心属性,下面我们就从这五个核心属性入手,一步一步实现计算总数案例的所有原有功能;

删除掉计算总数组件所有逻辑代码,此时demo,如下图:


代码如下:



单一数据源(state)

首先是state配置,它的值是一个对象,用来存储共享状态;Vuex使用单一树原则,将所有的状态都放到这个对象上,便于定位和维护;

我们把计算总数组件的count放到Vuex中的state中,如下:

...
const store =  new Vuex.Store({
  state: {
    count:0
  }, 
})
...

组件中获取,可以这样:

...
 computed:{
       count(){
           return this.$store.state.count
       }
  },
...

如果组件内需要引入的共享状态属性较多,都使用this.$store.state.x的话,会比较麻烦,所Vuex给我们提供了mapState()辅助函数来获取,如下:

import {mapState} from 'vuex'
 ...
 computed:{
  //参数为对象写法
   ...mapState({
     //key值随意自定义,模板中插值也要写入自定义的key值
       count:'count' 
   })
   //参数为数组写法
   // 此时组件定义的属性名要和state中定义的属性名一致
  ...mapState(['count'])
  },
  ...

以上两种获取方式,组件都可以获取到共享的count属性,并显示到页面上,如下图:

状态更新方式(mutations)

Vuex中的状态和组件中的状态不同,不能直接 state.count = ‘xx’ 这种方式去修改;Vuex修改状态的唯一方式就是提交mutation;

mutation是一个函数,第一个参数为state,它的作用就是更改state的状态;

下面我们就来定义一个mutation,在函数内去更新count这个状态:

const store =  new Vuex.Store({
  state: {
    count:0
  },
  mutations: {
    //更改count方法,type为增加
    JIA(state,count){
      state.count += count
    }
  }
})

组件中提交commit函数去触发mutattions中定义的JIA函数:

...
 methods:{
    jia(){
         this.$store.commit('JIA',this.value)
     }
 }
 ...

或者是利用最常用的mapMutations辅助函数,如下:

...
 //把获取的单选框的value值,通过参数传给mutation定义的jia函数,
 +
 ...
 import {mapMutations} from 'vuex'
 ...
  methods:{
     //对象参数中的key值为自定义的点击事件名
      ...mapMutations({JIA:'JIA'}),
      //参数为数组,此时点击事件名要和mutation中定义的增加事件名一致
       ...mapMutations(['JIA',]),
  }

以上两种方法都能实现增加功能,如下图:

store中的计算属性(getters)

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数,这里我们就简单的把计算总数和随之增加10倍:

Getter 接受 state 作为其第一个参数,如下:

const store =  new Vuex.Store({
  state: {
    count:0
  },
  getters:{
    multipleCount(state){
      return state.count * 10
    }
  }
})

组件中可以这样去获取,如下:

当前计算的和的10倍为:{{multipleCount }}

... computed:{ multipleCount(){ return this.$store.getters.multipleCount }, },

或者利用辅助函数mapGetters获取,和mapState引入一样,如下:

import {mapGetters} from 'vuex'
...
computed:{
    //对象写法
    ...mapGetters({
        multipleCount: 'multipleCount'
    }),
    
    //数组写法
    //此时自定义的函数名要和getter中定义的函数名一致
   ...mapGetters(['multipleCount']),
 },

以上两种方式都可以获取到getter函数,如下图:

异步更新状态(actions)

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

最大的区别就是action中可以写一些业务逻辑和异步操作;而mutation就是简单的变更状态;

怎么说呢,其实action很像一个连接组件和mutation的中介,对于同步和异步操作在action中都能完成,它只是帮我们在actions函数中commit提交了mutations中的函数;

同步增加总数

下面我们来再一次用action来实现点击JIA函数增加总数(同步操作),如下:

const store = new Vuex.Store({ state: { count:0 }, mutations: { JIA(state,count){ state.count += count } }, actions:{ //第一个参数为context(上下文),第二个参数为传的值 jia(context,value){ console.log(context) //打印结果如下图 context.commit('JIA',value) } },})

这就是 console.log(context) 的结果,是一个对象,里面包含了commit,dispatch,getters等方法,像一个小型的srore,但是跟store不一样,我们这里就叫上下文;

既然context是个对象,如果我们只需要commit的话,也可以使用数据解构的方式写,入下图:

 actions:{
   //第一个参数为context(上下文),第二个参数为传的值
    jia({commit},value){
        commit('JIA',value)
    }
  },

组件可以分发action,通过 store.dispatch 方法触发:

methods:{   
    jia(){
          this.$store.dispatch('jia', this.value)
      },
}

或者通过mapActions辅助函数来触发:

import { mapActions} from 'vuex'
...
methods:{   
   //对象参数中的key值为自定义的点击事件名
   ...mapActions({jia:'jia'}), 
   //数组参数
   //此时点击事件名要和action中定义的增加事件名一致
   ...mapActions(['jia']), 
}

我们利用actions和mutations都可以实现JIA函数的功能使总数增加;那问题来了,我们可以调用mutations里面定义的JIA函数,为什么还要多此一举的在actions中去调用mutation,然后再去分发actions呢;

当然这里我们只是演示给大家作为参考,一般同步操作我们也不会多此一举的放入actions里面,项目中直接更新状态我们就直接在组件commit提交mutation函数就可以;

异步增加总数

由于项目没有写后台服务,是mock的数据,这里我们异步就用定时器来写,如下图:

 actions:{
    asyncJia({commit},value){
        setTimeout(() => {
          context.commit('JIA',value)
        },1000) 
    }
 },

组件中分发action方法如下:

 methods:{
       asyncJia(){
           this.$store.dispatch('asyncJia',this.value)
        }
  }

一样,也可以利用mapActions函数,如下:

import { mapActions} from 'vuex'
...
methods:{   
   // //对象参数中的key值为自定义的点击事件名
   ...mapActions({asyncJia:'asyncJia'}), 
    //数组参数,此时点击事件名要和action中定义的增加事件名一致
   ...mapActions(['asyncJia']), 
}

不管是同步和异步操作,以上方法都能实现增加总数的功能;

为什么actions中处理异步

那问题来了,为什么actions中可以处理异步操作,而不能在mutations中进行呢?

以上属于简单的一些业务逻辑,action中的定时器或者奇数判断其实在mutation中就可以完成,这样感觉action就像是多余的一样;

官方明确表示:

  • mutations里同步的意义在于,每一个mutation执行完毕之后,可得到对应的状态,方便Vuex的devtools工具可以跟踪状态的变化
  • 如果在mutations中写入异步,devtools工具就没法知道状态是什么时候更新的,所以才有了actions
  • actions用来专门处理异步,里面触发mutations,就可以很清楚的看到mutation是何时被记录下来的,并且可以立即查看对应的状态,这样异步更新也可以看到更新状态的流程;

举个例子,还有就是遇到一些复杂的业务逻辑,第二个异步操作可能会依赖第一个操作的结果;

代码如下:

actions:{
    asyncJia({commit},value){
      return new Promise((resolve,reject) => {
        setTimeout(() => {
          context.commit('JIA',value)
          resolve()
        },1000)
      })  
    }
 },

组件内调用此异步增加函数后,返回结果后再等1秒调用增加函数,代码如下:

...
异步 + 后再等1秒后 +
...
import {mapMutations, mapActions} from 'vuex'
...
methods:{
     ...mapMutations(['JIA','JIAN']),
      ...mapActions(['oddJia','asyncJia']),
      doubleAsync(){
          this.asyncJia(this.value).then(() => {
            setTimeout(() => {
               this.JIA(this.value)
            },1000)
          })
      }
  }

demo案例如下图:

分工明确,这样看着清晰明了,操作起来也很方便;

扩展:其实在matations中也可以实现异步操作,或者不需要mutations,直接在action中去更改state状态,页面功能也都可以正常使用并显示;但是上面说到了,如果Vuex没有操作规则,大家都随心所欲的去编写逻辑代码,devtools工具也不会正确跟踪,那样也不便于后期维护,代码一样很乱,所以我们还是听从Vuex官方推荐的标准去使用;

还有一个modules模块没有介绍,不要着急,目前我们只是实现了单个组件与Vuex关联,只是介绍了核心属性的api用法,还没有实现多组件数据共享;

下面就把项目中两个组件的详细的Vuex版本代码分享出来,作为参考。

六. 使用Vuex组件的完整代码

store仓库代码

import Vue from 'vue'
import Vuex from 'vuex'
import {message} from 'element-ui'
Vue.use(Vuex)

const store =  new Vuex.Store({
  state: {
    count:0,
    filterTable:[]
  },

  getters:{
    multipleCount(state){
      return state.count * 10
    }
  },

  mutations: {
    JIA(state,count){
      state.count += count
    },
    JIAN(state,count){
      state.count -= count
      if(state.count <= 0) state.count = 0
    },
    ADDPERSON(state,person){
      state.filterTable.push(...person)
    },
     //这个重置方法本不需要,只是为了组件使用commit
     //dispatch('restPerson')就可以实现,这里为了下面讲解知识点而用
    RESTPERSON(state,person){
        state.filterTable = []
        state.filterTable.push(...person)
     }
  },

  actions:{
    asyncJia(context,value){
      return new Promise((resolve,reject) => {
        setTimeout(() => {
          context.commit('JIA',value)
          resolve()
        },1000)
      })  
    },
    oddJia({commit,state},value){
       if(state.count%2 !== 0){
        commit('JIA',value)
       }
    },
    addPerson({commit,state},obj){
      var isHave = true
      state.filterTable.forEach( e => {
          if(e.name == obj.value)  isHave = false
      })
      if(!isHave){
          message({
              message: '人员已添加!',
              type: 'warning',
              duration:1000
          })
          return
      }
      
      var person =  obj.options.filter( item => {
        return item.name == obj.value
      })
      commit('ADDPERSON',person)
    },
    restPerson({commit,state},value){
       state.filterTable = []
       commit('ADDPERSON',value)
    }
  },
})

export default store

计算总数案例

导航二计算总数案例组件代码,如下:



添加人员案例

导航三添加人员案例代码,如下:



Vuex实现组件间数据共享

现在就算我不去介绍,大家也会用了,现在两个组件所需彼此的数据已经放到了仓库的state中,只需要直接获取就行;

那我们就把两个组件一直存在的 ”???“ 给他们获取一下值;

计算总数组件中获取添加人员组件中表格的人员总数,如下:

   ...
   

此时导航三组件内总人数为:{{filterTable.length}}

当前计算的和为:{{count}}

... computed:{ //只需要引入state中的filterTable属性即可 ...mapState(['count','filterTable']) }, ...

添加人员组件中获取计算总数的值,如下:

 ...
 

此时表格总人数为:{{filterTable.length}}

此时导航二组件的计算总和为:{{count}}

... computed:{ filterTable(){ return this.$store.state.filterTable }, count(){ return this.$store.state.count }, }, ...

只要导航二计算组件中的计算总数值count发生变化,导航三添加人员组件内的计算总和也随之变化;

只要导航三添加人员组件表格数据发生变化,导航二计算总数组件内总人数也随之变化;如下图:

这样就是实现了组件间数据的共享,简单多了吧,你也可以像我这样去练习;

七. Vuex模块化编码(modules)

其实到目前为止,你已经基本上学会了Vuex的使用了,modules只是辅助我们把Vuex模块化,让我们的代码更清晰明了,便于维护和开发,提高发开效率;

想一下,目前我们只是有两个组件需要共享数组,看着没有问题,同样要是有十几个组件,几十个组件也需要共享数据呢,难道我们要把所有共享的状态和更改的业务逻辑都写在一个state,mutations和actions中吗,可以想到肯定很乱,怎么办,这就有了modules;

store模块化

如果要使用模块化编码的话,store中的代码编写会发生变化,组件引入的api不会没有改变,但是写法也会有所不同;

首先我们先来使store分成模块,目前我们有两个组件,每个组件的state,mutations和actions都是独立的,所以我们可以拆分两个模块:

在store文件夹下创建一个count.js文件,此文件只属于计算总数的状态和业务逻辑,最后记得暴漏出去,如下:

const countOptions = {
   //这里添加此属性,是为了后面使用map辅助函数简写时可以找到该模块
    namespaced: true,
    state: {
        count:0,
    },
    getters:{
      multipleCount(state){
         return state.count * 10
      }
    },
    mutations: {
      JIA(state,count){
          state.count += count
      },
      JIAN(state,count){
	      state.count -= count
	      if(state.count <= 0) state.count = 0
      },
    },

    actions:{
      asyncJia(context,value){
	      return new Promise((resolve,reject) => {
	          setTimeout(() => {
	            context.commit('JIA',value)
	            resolve()
	          },1000)
	      })  
      },
      oddJia({commit,state},value){
	      if(state.count%2 !== 0){
	          commit('JIA',value)
	       }
      },
    },
}
export default countOptions

在store文件夹下创建一个addPerson.js文件,此文件只属于添加人员的状态和业务逻辑,最后也要暴露出去,如下:

import {message} from 'element-ui'
const addPersonOption = {
    namespaced: true,
    state: {
        filterTable:[]
    },
    mutations: {
      ADDPERSON(state,person){
          state.filterTable.push(...person)
      },
     //这个重置方法本不需要,只是为了组件使用commit
     //dispatch('restPerson')就可以实现,这里为了下面讲解知识点而用
      RESTPERSON(state,person){
        state.filterTable = []
        state.filterTable.push(...person)
      }
    },
    actions:{
      addPerson({commit,state},obj){
	       var isHave = true
	       state.filterTable.forEach( e => {
	           if(e.name == obj.value)  isHave = false
	       })
	       if(!isHave){
	           message({
	               message: '人员已添加!',
	               type: 'warning',
	               duration:1000
	           })
	           return
	       }  
	       var person =  obj.options.filter( item => {
	         return item.name == obj.value
	       })
	       commit('ADDPERSON',person)
      },
      restPerson({commit,state},value){
	        state.filterTable = []
	        commit('ADDPERSON',value)
      }
    },
}
export default addPersonOption

在store/index.js文件里面引入上面两个模块组件,挂到如下modules中,如下:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

import countOptions  from './count'
import addPersonOption from './addPerson'

const store =  new Vuex.Store({
  modules:{
    countOptions,
    addPersonOption
  }
})
export default store

这样分成模块去开发,是不是,眼前一亮,清晰多啦;

模块化的组件调用

看到这里不知道你是否发现,我在计算总数组件里面用的都是map函数调用方法;在添加人员组件用的都是this.$store.xx的调用方法;之所以这样做就是为了现在使用模块化的时候给大家都全部介绍一下api在模块下的引入方式;

map辅助函数

命名空间(namespaced)+ map辅助函数的简单写法去调用,写法如下;

mapState函数的调用改为:

    computed:{
        ...mapState('countOptions',['count']),
        ...mapState('addPersonOption',['filterTable'])
    },

mapGetters函数的调用改为:

    computed:{
        ...mapGetters('countOptions',['multipleCount']),
    },

mapMutations函数的调用改为:

methods:{
     ...mapMutations('countOptions',['JIA','JIAN']),
 }

mapActions函数的调用改为:

methods:{
    ...mapActions('countOptions',['oddJia','asyncJia']),
 }

this.$store.xxx

获取模块化states方法,state后面加上模块名,如下:

computed:{
   filterTable(){
      return this.$store.state.addPersonOption.filterTable
   },
   count(){
      return this.$store.state.countOptions.count
   },
},

获取模块化mutations方法,在mutation函数名前加上 “模块名+/”,如下:

   methods:{
       initTable(){
            var person = this.filterTable.length == 0 ? this.tableData  :  this.filterTable
            this.$store.commit('addPersonOption/RESTPERSON',person)
        },
   }

为了了解获取模块化getters的调用方法,我们给Vuex的addPersonOption模块添加一个getters方法获取表格第一个人的名字,组件调用展示,代码如下:

 getters: {
      onePersonName(state){
         return state.filterTable[0].name
      }
 },
 computed:{
       onePersonName(){
           return this.$store.getters.['addPersonOption/onePersonName']
       }
  }

获取模块化actions方法,如下:

 methods:{
     addPerson(){
          var obj = {
              value:this.value,
              options:this.options
          }
          this.$store.dispatch('addPersonOption/addPerson',obj)
      },
      rest(){
          this.$store.dispatch('addPersonOption/restPerson',this.tableData)
      }
 }

再给大家看一下demo,修改后,功能正常使用!完美!,图如下:

八. 总结

好了,以上就是关于Vuex的所有知识点,因为最近又刷了一遍尚硅谷Vue的视频,为了练习和总结,就写了这篇将近18000字的关于Vuex博客,应该算是很详细,很详细啦!

到此这篇关于一篇文章带你吃透Vuex3的状态管理的文章就介绍到这了,更多相关Vuex3状态管理内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

-六神源码网 -六神源码网