简述

欢迎来到新的篇章,Vuex,Vuex是一个专为vue应用程序开发的状态管理工具,和vue-router一样是vue的插件
看到状态管理,你可能会想到vue-router篇中的keep-alive,好像也是用来状态管理。
这里解释一下两者的区别:

keep-alive是组件对象本身的状态管理,是当我点击按钮,路径跳转,切换组件时,原有的组件对象仍然存在
我切换回去的时候,vue不会去重新创建新的组件对象,而是使用原来的那个组件对象,这就是为什么当我使用了
keep-alive包裹住router-view后,组件的created和destroy钩子函数失效,因为这两个钩子函数就表示vue组件
创建和销毁,使用了keep-alive后,切换组件执行的是activated和deactivated钩子函数。

而Vuex的状态管理,我们可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面,
然后将这个对象放在顶层的vue实例中,让其他组件可以使用,那么多个组件就可以共享这个对象中的所有变量方法了。
你可能想到了vue的原型对象,但是vue的原型对象虽然能做到组件间变量方法共享,但是我们还需要这些变量和方法
是响应式的,当某个组件修改了共享变量或方法时,其他组件引用的共享变量或方法及时更新,但是使用vue原型对象
却不能做到响应式,所以vuex就出现了,解决多个组件之间共享变量和方法,并且这些变量和方法是响应式的。

那么,有什么状态需要我们在多个组件间共享的呢?

比如用户的登录状态,用户名称,头像,地理位置信息等等,比如商品的收藏,购物车中的物品等等,
这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的

下面看看Vuex具体需要学习哪些内容:

Vuex基本使用

  • 安装Vuex,和router一样,安装好插件后,在项目的src目录下创建store目录,并在其中创建index.js作为
    Vuex的配置文件,下面是该配置文件的具体内容:

    npm install vuex -S		//-S就等于--save,表示运行依赖,简写而已,同样的-D是--save-dev的简写
    
    //store/index.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)	//注册插件
    
    export default new Vuex.Store({
      state: {
      },
      mutations: {
      },
      actions: {
      },
      modules: {
      },
      getters: {
      }
    })
  • 然后在入口函数main.js中的vue实例中挂载即可,下面是main.js具体内容

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    /*
        阻止启动生产消息,没有这句,如果是开发时打包启动程序,会打印大量消息,
        所以开发时关闭该选项,生成模式时再打开
    */
    Vue.config.productionTip = false
    /*
    	把router,store传递到vue实例中,
        则相当于Vue.prototype.$store=store
        Vue.prototype.$router=router
    */
    new Vue({
        router,
        store,
        render: h => h(App)
    }).$mount('#app')
    /*
    	上面相当于:createApp(App).use(store).use(router).$mount('#app')
    */
  • 上面是Vuex的基本框架,我们把组件中共同的变量方法抽取到一个对象中,那么在Vuex中,
    这个对象就是在index.js配置文件中导出Vuex实例的参数,这个参数是就是存放共同变量和方法的对象
    这个对象中,有很多属性,state,mutations,modules,getters等等,其中state属性就是存放共享变量的
    下面就举个栗子:

    //假设现在,我的app.vue和helloVuex.vue有相同的变量,我们可以把共享变量放在store的state中
    //只需要在state中增加变量即可
    state: {
    	count: 999
    }
    
    /*
    	那么在app.vue和helloVuex.vue中通过{{$store.state.count}}就可以访问到这个变量
    	如果是组件对象的方法访问,则需要加上this,发现是不是跟vue-router取值$router方式很像?
    	可以肯定,当Vuex挂载到vue实例中后,会有一句Vue.prototype.$store=store,
    	所以我们在其他组件就能使用$store获取共享变量
    */

Vuex核心概念

Vuex核心概念主要就是讲讲存放共享变量和方法的对象属性的,state,mutations,modules,getters这些属性
有哪些作用及其使用,让我们开始吧:

  • state
    上面简单的案例中使用过,state就是存放共享变量的,除此之外,Vuex提出单一状态树的概念
    意思是在Vuex配置文件store/index.js中,只创建一个Vuex实例,例如下面的代码:

    export default new Vuex.Store({
      state: {
      },
      mutations: {
      },
      actions: {
      },
      modules: {
      },
      getters: {
      }
    })
    /*
    	推荐只创建一个Vuex对象(store),项目中所有共享变量都放在这个对象的state,而不是创建多个Vuex实例,
    	把共享变量分开存放到不同的state,这样做的好处是方便项目的管理和维护。
    */
  • mutations
    mutations用于修改state中的变量的,我们在app.vue和helloVuex.vue中通过$store.state.count可以
    访问的到共享变量count,你可能会通过$store.state.count++的方式使得共享变量+1,但是这种修改共享
    变量的方式并不推荐,mutations是唯一能够修改state变量的方式,如果想要修改共享变量,你需要在
    mutations中定义方法,然后在组件中去调用mutations中的方法,达到修改共享变量的目的,下面是具体代码:

    //store/index.js,vuex配置文件
    export default new Vuex.Store({
      state: {
          count: 999	//共享变量
      },
      mutations: {
          increment(state){
            state.count++;
          },
          decrement(state){
              state.count--;
          },
          become100(state,data){	//data接收组件传递过来的数据
              state.count=data;
          }
      },
      actions: {
      },
      modules: {
      },
      getters: {
      }
    })
    
    //在HelloVuex组件中使用mutatios的方法
    <button @click="add">+</button>
    <button @click="jian">-</button>
    <button @click="become100">将共享变量变成100</button>
    <script>
      export default {
        name: "HelloVuex",
        methods: {
          become100(){
            this.$store.commit('become100',100)	//调用mutations中become100方法并传递数据100
          },
          add(){
            this.$store.commit("increment");	//调用mutations中的increment方法
          },
          jian(){
            this.$store.commit("decrement");
          }
        }
      }
    </script>
    /*
    	在mutations中定义好修改state的方法,在组件中通过$store的commit方法调用mutations中的方法
    	如果你想在组件中传递数据到mutations中的方法,可以在mutations方法中增加一个data参数
    	用于接收组件传递过来的数据,这个参数被称为mutations的载荷(payLoad),参数名随意
    	而参数state就是state属性值,可以拿到所有的共享变量。
    */

    mutations中有几处需要说明的地方

    • mutations的提交方式
      除了使用this.$store.commit('become100',100)方式调用mutations中的方法传递数据,
      你还可以传递一个对象来调用mutations中的方法和传递数据,例如:
      this.$store.commit({type: 'become100',count: 100}),两种提交方式的区别是:
      如果是第一种方式,则mutations方法的data参数是实际的数据,即100
      如果是传递对象的方式提交,则mutations方法的data参数是你传递的对象,你想获取count数据
      需要data.count才能拿到,例如下面这样:

      become100(state,data){  //这个data就代表你传递的对象,即{type: 'become100',count: 100}
          state.count=data.count;
      }
    • mutations响应规则
      响应规则你需要遵守一定规则,才能使得数据state中共享变量数据被更改后,界面上也跟着更改,
      即,遵守一定规则才能使得数据是响应式的:

      1)提前在store中初始化好所需的属性,如果在mutations方法中新增共享变量,则也不是响应式的
      2)当给state中的对象(状态)添加新属性时,使用下面的方式
      	方式一:使用Vue.set(obj,'newProp',123),(如果是后来新添加的属性使用该方法,不然数据不会被响应到界面中)
      		obj.newProp=123,这种方式,不是响应式的!!!即数据改变了,但是界面上不能响应
      	方式二:用新对象给旧对象重新赋值
      3)如果是删除对象属性,不能使用delete obj.oldProp,这种也不是响应式的删除对象属性的方法
      	你需要使用:Vue.delete(obj,'oldProp')

      之前提到,修改数组不能直接通过arr[0]=100这种方式修改数组中数据,这种方式不是响应式的,
      其实这里的规则同样是vue中修改数据的规则

    • mutations常量类型
      mutations可以被分为两部分:字符串的事件类型(type),一个回调函数,该回调函数的第一个参数就是state,如下所示:

      mutations: {
          //increment是事件类型(跟我们熟悉的事件没关系),后面的部分就是回调函数了
          increment(state){
              state.count++;
          }
      }

      在mutations中,我们定义了很多事件类型(也就是mutations的方法名),当mutations中方法越来越多时,
      使用者需要花费大量的精力去记住这些方法,并且还可能敲错,我们可以将这些事件类型定义为常量,放到一个文件中
      然后再在Vuex配置文件中引用这个常量,如下所示:

      increment(state){
          state.count++;
      }
      变成
      import {INCREMENT} from './xxx/xxx.js'
      [INCREMENT](state){
          state.count++;
      }
      //不过我感觉更加麻烦了,不知道实际开发中有没有用

    最后说下,为什么一定要调用mutations方法才能更改state属性,而不直接引用state中变量进行修改
    这里有一张Vuex状态管理图片,有时候我们需要查看是哪个组件修改了共享变量,如果直接修改state中共享
    变量,则我们根本不知道哪个组件修改了,而如果我们使用mutations方法修改共享变量,那么通过浏览器
    的devtools插件就能查看到是哪个组件修改了共享变量。

  • getters
    类似计算属性,如果数据是组件自己的数据,并且这个数据要经过一些处理才能输出到页面,则要使用计算属性
    当这个数据是共享变量时,就使用getters,对了,计算属性有缓存,这个getters大概率也是有缓存的
    来看下具体使用代码:

    //index.js,vuex配置文件中
    export default new Vuex.Store({
      state: {
        count: 999
      },
      mutations: {
        increment(state){
            state.count++;
        },
        decrement(state){
          state.count--;
        },
        become100(state,data){
          state.count=data;
        }
      },
      getters:{
        //将count相乘
        toDouble(state){
          return state.count*state.count
        },
        //将相乘的数再加100
        toDoubleAnd100(state,getter){	//通过getter参数可以调用其他getter方法
          return getter.toDouble+520
        },
        //拿到state中的count的值,将该值与调用的值相加,即创建个闭包
        addYouValue(state){
          return function(value){
            return state.count+value
          }
        }
      }
    })
    
    //HelloVuex.vue中使用
    <h2>{{$store.getters.toDouble}}</h2>
    <h2>{{$store.getters.toDoubleAnd100}}</h2>
    <h2>{{$store.getters.addYouValue(-5)}}</h2>

    在使用getters有两个需要注意的地方:

    • 上面例子中,getters方法可以有两个参数,第一个state参数,拿到共享变量,第二个getters则是getters
      属性值,通过这个getters参数可以使用其他getter方法
    • getters方法可以返回函数,即闭包,而返回的函数的参数,则可以由外界控制,就像上面例子的addYouValue方法一样,
      通过$store.getters.addYouValue拿到闭包,然后使用(-5),去调用这个闭包。
      可以想象,既然store的getters可以使用闭包,那么计算属性可以创建闭包,
      利用了计算属性的调用不需要括号的特性,此时如果带上括号,则就是闭包的使用
  • actions
    通常情况下,vuex要求我们mutations中的方法必须是同步方法,主要原因是当我们使用devtools时,
    devtools可以帮助我们捕捉mutations的快照,但是如果是异步操作,那么devtools将不能很好的追踪
    这个操作什么时候会被完成,actions就是专门处理异步请求的地方。
    actions方法与mutations方法,一个是执行异步操作的地方,一个是执行同步操作的地方,
    在使用上还有很多差别,让我们来看看具体有哪些不同:

    1)action方法中的方法参数是context,而mutations中方法参数是state,context指的是store对象,
    	同样的,action的方法参数也可以有两个,用于接收使用者传递的参数,这和mutations类似
    2)action方法中不能修改state中的共享变量,只能通过调用mutations中的方法修改变量,
    	context.commit('mutations中的方法')	//就像$store.commit()一样
    3)使用者在调用action中方法时:this.$store.dispatch('action中方法名','携带的信息'),
    	而mutations使用的是$store.commit方法调用
    4)action方法中的异步操作可以返回Promise对象,如果使用了promise,使用者在调用action方法时
    	可以跟上.then()方法,这个then方法可以用来判断异步操作是否成功。

    下面是一个使用actions的案例:

    // store/index.js,Vuex配置文件
    export default new Vuex.Store({
      state: {
        count: 999
      },
      mutations: {
        increment(state){
            state.count++;
        }
      },
      actions: {
        asyncTest(context,message){
          return new Promise(resolve=>{	 //创建一个Promise,这个对象会在下一篇axios重点讲
            setTimeout(()=>{	//定时器,模拟异步操作
              console.log(message);
              context.commit('increment')	//调用mutations的increment方法
              resolve("异步操作完成")	 //调用resolve会去执行promise的then方法
            },1000)
          })
        }
      }
    })
    
    //组件内,使用action中方法
    <button @click="asyncTest">测试异步</button>
    methods: {
      asyncTest(){
        this.$store
            .dispatch('asyncTest',"正在执行异步操作")
            .then((res)=>{
            	console.log(res);
            });
        }
    }
    /*
    	点击按钮,执行asyncTest方法,其中使用$store.dispatch执行actions中的asyncTest方法
    	并向该方法中传递参数,这个方法返回一个promise对象,执行定时器后,调用mutations中increment方法
    	接着执行resolve并传递参数,这个方法实际上执行的是组件中then方法;
    */
  • modules
    当应用变得非常复杂时,store对象就有可能变得相当臃肿,为了解决这个问题,vuex允许我们将store分割成模块
    而每个模块拥有自己的state,mutations,actions,getters,但是呢,模块中定义的state,使用者在使用时还是有些区别的

    没有使用模块,取值时:$store.state.name
    使用了模块:
    	//index.js,vuex配置,在store实例的module中定义a模块
        modules: {
            a: {
                state: {
                    name: '万一'
                }
            }
        }
    	//使用者取值时,需要选择哪个模块
    	<h2>{{$store.state.a.name}}</h2>

    另外模块中的getters,actions和外部的getters,actions方法还是有些区别的:

    1)模块中的getters还可以接收第三个参数,这个参数是外部的state,你可以给这个参数取名为rootState,
    	表示模块中能拿到store实例的state中的共享变量
    2)模块中actions的context参数调用commit方法只能调用模块内的mutations,
    	而不能调用外部的mutations中的方法,但是模块中actions的context中可以拿到RootGetters,RootState
    	表示可以调用外部的state的共享变量和getters中的方法。

Vuex的文件组织

目前我们的配置全是放在store中的index.js中,我们可以将store实例中的getter,actions,modules,mutations全部抽离出来
放在src/store目录下,除了state不抽取成js文件外,其他配置全部抽取成js文件,使用es6的导入导出配置,
需要注意的是抽取的modules可能会有很多个,所以抽取的modules最好放在store/modules/moduleA.js
创建个modules目录放在其中最好。

2021年10月3日更新,看了尚硅谷的vue网课,感觉天禹老师讲的太好了,
关于vuex的模块化,请点击链接:尚硅谷vue网课,P115,P116两节。

mapState,mapGetter等的使用

vuex提供了四个函数可以帮我们简化代码的编写,分别是mapState,mapGetters,mapMutations,mapActions
通过练习,我感觉使用这些函数的作用相当于将共享对象中的state,getters等方法,数据都导入组件中,
实际上是根据共享变量中的state,getters等属性生成新的函数绑定到组件上,简化了代码编写,不使用vuex
提供的函数依然可以实现功能,这个在接下来的案例中你可以感受一下:

//App.vue
<template>
  <div id="app">
    <wan-yi/>
  </div>
</template>
<script>
  import WanYi from './components/WanYi';
  export default {
      components: {
        WanYi,
      }
    }
</script>

//axios/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  /*共享对象中的数据*/
  state: {
    msg: '万一梦想实现了呢?',
    school: '湖北师范大学',
    address: '湖北黄石'
  },
  /*共享对象中更改数据*/
  mutations: {
    changeMsg(state,data){
      state.msg=data
    },
    changeSchool(state,data){
      state.school=data
    },
    changeAddress(state,data){
      state.address=data
    }
  },
  /*共享对象中的计算属性*/
  getters: {
    getter1(state){
      return "中国-"+state.address
    },
    getter2(state,getter){
      return "地球-"+getter.getter1
    },
    getter3(state,getter){
      return "银河系-"+getter.getter2
    }

  },
  actions: {
    action1(context,data){
      return new Promise(resolve => {
        console.log("执行异步更改msg任务。。。。");
        setTimeout(()=>{
          context.commit("changeMsg",data)
          resolve("异步任务已完成")
        },1000)
      })
    },
    action2(context,data){
      return new Promise(resolve => {
        console.log("执行异步更改school任务。。。。");
        setTimeout(()=>{
          context.commit("changeSchool",data)
          resolve("异步任务已完成")
        },1000)
      })
    },
    action3(context,data){
      return new Promise(resolve => {
        console.log("执行异步更改address任务。。。。");
        setTimeout(()=>{
          context.commit("changeAddress",data)
          resolve("异步任务已完成")
        },1000)
      })
    },
  },
})

//WanYi.vue
<template>
  <div>
    WanYi组件内:<br>
    **************************测试mapState*****************************************<br>
    <!--传统方式拿到共享对象state中的数据-->
    {{$store.state.msg}}
    {{$store.state.school}}
    {{$store.state.address}}
    <br/>
    <!--使用mapState拿到共享对象中state数据,并生成相应的对象形式,对象写法-->
    {{msg2}}
    {{school2}}
    {{address2}}
    <br>
    <!--mapState的数组写法使用-->
    {{msg}}
    {{school}}
    {{address}}
    <br/>

    **************************测试mapMutations*****************************************<br>
    <!--传统方式调用mutations中的异步方法-->
    <button @click="mutationsTest1">传统方式调用mutations中的异步方法</button>
    <br>
    <!--使用mapMutations,对象写法-->
    <button @click="mutationsTest2">mapMutations方式使用mutations,对象形式</button>
    <br>
    <!--mapMutations数组写法使用-->
    <button @click="mutationsTest3">mapMutations方式使用mutations,数组形式</button>
    <br>
    **************************测试mapGetters*****************************************<br>
    <!--传统方式使用getters-->
    {{$store.getters.getter2}}
    {{$store.getters.getter3}}
    <br>
    <!--mapGetter对象形式-->
    {{two}}
    {{three}}
    <br>
    <!--mapGetter数组形式-->
    {{getter2}}
    {{getter3}}
    <br>
    **************************测试mapActions*****************************************<br>
    <!--传统方式调用actions中的异步方法-->
    <button @click="actionsTest1">传统方式调用actions中的异步方法</button><br>
    <!--mapActions使用actions中的异步方法,对象形式-->
    <button @click="actionsTest2">mapActions使用actions中的异步方法,对象形式</button><br>
    <!--mapActions使用actions中的异步方法,数组形式-->
    <button @click="actionsTest3">mapActions使用actions中的异步方法,数组形式</button><br>
  </div>
</template>

<script>
import {mapState,mapGetters,mapActions,mapMutations} from 'vuex'
export default {
  name: "WanYi",
  methods: {
    /*传统方式使用mutations更改数据*/
    mutationsTest1(){
      this.$store.commit('changeMsg','我的梦想在哪里?')
      this.$store.commit('changeSchool','苏州大学')
      this.$store.commit('changeAddress','浙江省')
    },

    /*mapMutations对象写法*/
    ...mapMutations({changeMsg2: 'changeMsg',changeSchool2: 'changeSchool',changeAddress2: 'changeAddress'}),
    mutationsTest2(){
      /*
        这里是直接在methods中执行导入的函数,你可以在DOM中设置一个点击按钮,回调这些函数,
        例如:<button @click="changeMsg2()"></button>
        注意:就算没有参数要传,你还是要写上(),不然会出错,因为mapMutations等函数在帮我们创建方法的时候是下面这样的
          changeMsg2(value){
            this.$store.commit('changeMsg',value)
          }
        如果你使用changeMsg2的时候,没有写上(),则就会传递一个event对象作为参数,
        所以在DOM中调用这些函数就算没有参数传递,也必须写上()
      */
      this.changeMsg2("我的梦想在哪里?")
      this.changeSchool2("苏州大学")
      this.changeAddress2("浙江省")
    },

    /*mapMutations数组写法,要求是:生成的方法要与共享对象中的mutations中方法名相同*/
    ...mapMutations(['changeMsg','changeSchool','changeAddress']),
    mutationsTest3(){
      this.changeMsg("学习?学个屁")
      this.changeSchool("家里蹲大学")
      this.changeAddress("银河系第三宇宙")
    },

    /*传统方式调用actions中的异步方法*/
    actionsTest1(){
      this.$store.dispatch('action1','光在哪里?')
        .then(data=>{
        console.log(data);
      })
    },

    /*mapActions方式使用actions中方法*/
    ...mapActions({updateSchool:'action2'}),
    actionsTest2(){
      this.updateSchool("中国科学院大学").then(data=>{
        console.log(data);
      })
    },
    /*mapActions方式使用actions中方法*/
    ...mapActions(['action3']),
    actionsTest3(){
      this.action3("彭泽县南阳村").then(data=>{
        console.log(data);
      })
    }
  },
  computed: {
    /*从共享变量的state中读取数据,并生成响应的计算属性,这是对象写法*/
    ...mapState({msg2:'msg',school2: 'school',address2: 'address'}),
    /*同上,不过是数组写法,这种写法要求生成的计算属性名与state中共享变量名相同*/
    ...mapState(['msg','school','address']),

    /*mapGetter的对象形式*/
    ...mapGetters({two:'getter2',three:'getter3'}),
    /*mapGetter的数组形式,数组形式要求生成的计算属性名和getters中的函数名要相同*/
    ...mapGetters(['getter2','getter3']),

  }
}
/*
	小提示:
		...是es6中的扩展运算符,可以将对象中的属性和方法赋值给另一个对象,例如:
		let obj = {a:1,b:2}
		let obj2={
			c: 3,
			d: 4,
			...(obj)
		}
		console.log(obj2) //{a:1,b:2,c:3,d:4}
		上面的案例就是将新生成的函数赋值到computed,methods对象中
*/
</script>
<style scoped>
</style>