Vuex详解一:彻底弄懂state、mapState、mapGetters、mapMutations、mapActions

10,442 阅读4分钟

[toc]

在写这篇文章之前,在使用Vuex开发的时候,总是一知半解的,不会的就百度,复制粘贴后能跑就行,对于其里面的细节,也不是很了解。

今天借着项目踩坑的时机,查了很多资料,于是想把Vuex从头到尾说清楚。

看该文章之前,希望你对Vue和Vuex的有一定的基础知识,如果没有,请移步Vue官网

里面难免写的有错误,各位大佬看官不要介意,希望能指出错误~~

一 、state

先看一下标准的store目录结构

入vuex 以后,我们需要在state中定义变量,类似于vue中的data,通过state来存放共享的1状态

-store
 --actions   
 --mutations
 --getters
 --mutations-type
 --index.js
 

index.js是总入口

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

import state from './state.js'
import actions from './actions.js'
import mutations from './mutations.js'
import getters from './getters.js'


export default new Vuex.Store({
  state,
  actions,
  mutations,
  getters
  
})

我们在state.js中定义变量

export default {

  userInfo:{
	userName:"油炸皮卡丘",
	age:"22",
	sex:"女"
  },
  token:"aerfrftregtyrb.rytyuhjyi",
  friends:["Tom","Herry","Lucy"],
  likes:["吃饭",'睡觉','看电影'],
  money:250
}

在组件中使用,在App.vue中自定义两个组件

<div id="app">
    <about> </about>
    <home> </home>
</div>

about.vue里面的内容

<h1>{{$store.state.userInfo.userName}}</h1> //油炸皮卡丘

home.vue里面的内容

<h1>{{$store.state.userInfo.age}}</h1> //22

如图,显示出显示出相应的内容,有了vuex,我们不必在考虑组件之间的传值,直接就可以通过$store来获取不同的数据

但是如果需要vuex中的多个数据的这时候,这样写就太啰嗦了,我们可以将它定义在computed中。

propsmethods,datacomputed的初始化都是在beforeCreatedcreated之间完成的。

例如

<template>
  <div class="home">
    {{userName}}
  </div>
</template>
<script>
export default {
  name: 'home',
  computed:{
    userName(){
      return this.$store.state.userInfo.userName
    }
  }
}
</script>

这样引入就方便了很多。

1.2 mapState辅助函数

使用this.$store.state虽然可以很方便的将state里面的值融入computed,但是如果要取多个值,就会出现以下的情况

computed:{
    userInfo(){
      return this.$store.state.userInfo
    },
    
   token(){
      return this.$store.state.token
    },
    
     friends(){
      return this.$store.state.friends
    },
    
    likes(){
     return this.$store.state.likes
    },
    ...
  }

特别多的话就会很麻烦,而这时候vuex又给我们提供了更简便的方法mapState方法,

该方法就可以自动将需要的state值映射为实例的计算属性

我们可以这样写

import {mapState} from 'vuex'
export default {
  name: 'home',
  computed: mapState(['likes','friends','token','userInfo'])
}
mapState(['likes','friends','token','userInfo']) //填入哪些字段就会将这些字段自动映射为一个计算属性

所以

 mapState(['likes','friends','token','userInfo'])

等价于下面的代码

userInfo(){
  return this.$store.state.userInfo
},

token(){
  return this.$store.state.token
},

 friends(){
  return this.$store.state.friends
},

likes(){
 return this.$store.state.likes
},

记住:用mapState等这种辅助函数的时候,前面的方法名和获取的属性名是一致的。

如果我们需要自定义一个计算属性怎么办呢?怎么添加呢?

毕竟现在已经成这样了 computed: mapState(['likes','friends','token','userInfo'])

这时候我们就需要es6中的展开运算符:...。将mapState映射的计算属性追加上去,而不是覆盖原有的

computed: {   
  value(){
   return this.val++
  },
  ...mapState(['likes','friends','token','userInfo'])

}

在模板中使用

<h2>{{userInfo.userName}}</h2> //油炸皮卡丘

关于别名

有时候,我们映射的时候,想给计算属性起个新的名字,不想用原有的属性名,那就可以像下面这样做,用对象的形式去别名

computed: {   
  value(){
   return this.val++
  },
  ...mapState({
      myLikes:"likes",
      myFriends:"friends",
      theUserInfo:"userInfo"
  })

}

这样我们就可以在模板中使用

<h2>{{theUserInfo.userName}}</h2> //油炸皮卡丘

二、getters

getters相当于vue中的计算属性,能拿到state里面最新的值

而且getters允许传参,第一个参数就是state

这样,通过getters进一步处理,得到我们想要的值,

getters.js

export default{
    
    realName:(state)=>{
      return state.userInfo.userName+''
    },
    myMoney:(state)=>{
      return (state.money/7).toFixed(2)
    }
}

在实例中我们可以这样用

computed: {  
 valued(){
   return this.value++
 },
 realName:this.$store.getters.realName,
 myMoney:this.$store.getters.myMoney,
}

2.2 mapGetters辅助函数

mapGetters函数具有mapState的作用,而且其主要用法也是一样的,也能将getters的属性映射到实例的计算属性

computed: {  
 valued(){
   return this.value++
 },
 ...mapGetters(['realName','myMoney'])
}

同样也可以取别名

computed: {  
 valued(){
   return this.value++
 },
 ...mapGetters({
     myRealName:"realName",
     myFormatMoney:"myMoney"
 })
}

但有人就要问了,二者的作用实际上都一样?到底用哪个?或者说二者还有什么实质上的区别?

上面说过,getters可以传参,这才是getters和mapState的区别

那么如何传参呢?

getters的第一个参数是state

有这样一个场景,我们不想取所有的state里面的friends值,而是想根据我们传入的值筛选一下

那我们只要在getter返回的值套上一层函数那我们就可以实现传参数

export default{
    
    realName:(state)=>{
      return state.userInfo.userName+''
    },
    myMoney:(state)=>{
      return (state.money/7).toFixed(2)
    },
    
    myFriend:(state)=>{
         //返回一个函数用于接收我们传入的筛选值
         return function (friendName) {
           return state.friends.find(item => item === friendName)
       }
    }
}

很显然,getters不仅可以像mapState一样拿到state里面的值,而且还可以在拿到之前,对值进行我们想要的加工

从而“派生”一些新的值,以满足我们不同的业务需要

淡然,如果不需要派生新的值,

this.$store.getters.值  就等于 this.$store.state.值

三、mutation

我们代码中定义的时候需要些mutations,它类似于vue中的methods,

mutations需要通过commit来调用其里面的方法,它也可以传入参数,第一个参数是state,第二个参数是载荷(payLoad),也就是额外的参数

我们只能通过mutation去更改state里面的值

mutations.js

export default {
    addAge:(state,payLoad)=>{
     state.userInfo.age+=payLoad.number
   },
   
   changeName:(state,payLoad)=>{
     state.userInfo.userName+=payLoad.userName
   },
   
   addFriend:(state,payLoad)=>{
      if(state.friends.indexOf(payLoad.newFriend) < 0){
          state.friends.push(payLoad.newFriend)
      }
   },
   
   setUserInfo:(state,payLoad)=>{
       state.userInfo=payLoad.userInfo
   },
   
   setToken:(state,payLoad)=>{
       state.token=payLoad.token
   }
    
}

在模板中使用

<div class="home">
   <button @click="handleAddAge">增加一岁</button>
   <button @click="handleAddFriend">增加一个朋友</button>
</div>

js部分

methods:{
 handleAddAge(){
 //使用mutation不像之前state、getters一样直接点调用,而是用commit关键字来提交mutation
   this.$store.commit('addAge',{
     number:1
   })
 },
 
 handleAddFriend(){
     let name="皮卡丘";
     this.$store.commit('addFriend',{
     newFriend:name
   })
     
 }
}

调用的时候第二个参数最好写成对象形式,这样我们就可以传递更多信息。

this.$store.commit('mutationName',{
  key1:val1,
  key2:val2,
  key3:val3
})
     

但是,这样写还是会遇到同样的问题,就是如果需要操作多个数据,就会变的麻烦,这时候我们就需要mapMutations,通过它将方法映射过来

3.1 mapMutations辅助函数

跟mapState、mapGetters一样

不同的是,mapMutations是将所有mutations里面的方法映射为实例methods里面的方法

所以我们可以这样用

methods:{
 ...mapMutations(['addAge'])
}

mapMutations(['addAge'])这一句就相当于下面的代码


methods:{
 addAge(payLoad){
 
  this.$store.commit('addAge',payLoad)
 }
}

参数我们可以在调用这个方法的时候写入

<button @click="AddAge({number:1})">增加一岁</button>

同样也可以有别名

methods:{
 ...mapMutations({
     handleAddAge:'addAge'
 })
}
<button @click="handleAddAge({number:1})">增加一岁</button>

这时候有些人要说了,我为什么要绕一圈,从mutations里面去改state呢?我能不能直接改state呢?

比如这样:

addAge(){
 this.$store.state.userInfo.age +=5;
}

实际看结果也可以,那我为什么从mutations里面中转一下呢?

原因如下:

  • 在mutations中不仅仅能做赋值操作

  • Vue.js在mutations中做了类似埋点操作,如果从mutations中操作的话, 能被检测到,可以更方便用调试工具调试,调试工具可以检测到实时变化,而直接改变state中的属性,则无法实时监测

注意:mutations只能写同步方法,不能写异步,比如axios、setTimeout等,这些都不能写,mutations的主要作用就是为了修改state的。

原因类似:如果在mutations中写异步,也能够调成功,但是由于是异步的,不能被调试工具追踪到,所有不推荐这样写,不利于调试,这是官方的约定。

3.2 使用常量替代Mutation事件类型

把原本的方法名称由字符串转变成常量

mutations.js


const ADD_AGE ='addAge'
const CHANGE_NAME ='changeName'
const ADD_FRIEND='addFriend'
const SET_USER_INFO='setUserInfo'
const SET_TOKEN='setToken'

//然后用常量替换原有的方法名

export default {
    [ADD_AGE](state,payLoad){
     state.userInfo.age+=payLoad.number
   },
   
   [CHANGE_NAME](state,payLoad){
     state.userInfo.userName+=payLoad.userName
   },
   
   [ADD_FRIEND](state,payLoad){
      if(state.friends.indexOf(payLoad.newFriend) < 0){
          state.friends.push(payLoad.newFriend)
      }
   },
   [SET_USER_INFO](state,payLoad){
      state.userInfo=payLoad.userInfo
   },
   
   [SET_TOKEN]:(state,payLoad)=>{
       state.token=payLoad.token
   }
    
}

为什么这样写?

  • 不容易写错,字符串容易写错,而且字符串写错以后不会报错位置,而用常量替代,如果写错,eslint可以提示错误位置
  • 当使用action派发mutation时,在action中使用同样的常量,避免手滑写错方法

用常量替代mutations的时候我我们可以新建一个文件(mutation_type.js)专门存储这些常量

mutation_type.js部分


const ADD_AGE ='addAge'
const CHANGE_NAME ='changeName'
const ADD_FRIEND='addFriend'
const SET_USER_INFO='setUserInfo'
const SET_TOKEN='setToken'

export  {
   ADD_AGE,
   CHANGE_NAME ,
   ADD_FRIEND,
   SET_USER_INFO,
   SET_TOKEN
}

然后在mutations.js中引入

import {
    ADD_AGE,
    CHANGE_NAME,
    ADD_FRIEND,
    SET_USER_INFO,
    SET_TOKEN
} from "./mutation_type.js"

export default {
    [ADD_AGE](state,payLoad){
     state.userInfo.age+=payLoad.number
   },
   
   [CHANGE_NAME](state,payLoad){
     state.userInfo.userName+=payLoad.userName
   },
   
   [ADD_FRIEND](state,payLoad){
      if(state.friends.indexOf(payLoad.newFriend) < 0){
          state.friends.push(payLoad.newFriend)
      }
   },
   [SET_USER_INFO](state,payLoad){
      state.userInfo=payLoad.userInfo
   },
    [SET_TOKEN]:(state,payLoad)=>{
       state.token=payLoad.token
   }
    
}

四、actions

action类似于mutation

我们只需要记住一下几点:

  • action可以提交mutation,然后mutation去更改state
  • action不要直接去操作state,而是去操作mutation
  • action包含异步操作,类似于axios请求,可以都放在action中写
  • action中的方法默认的就是异步,并且返回promise

为什么?因为这是Vuex规定的

actions.js

import {
    ADD_AGE,
    CHANGE_NAME,
    ADD_FRIEND,
    SET_USER_INFO,
    SET_TOKEN
} from "./mutation_type.js"

export default{

   
   //定义一个异步获取用户信息的action
    
    async getUserInfo(context){
       //context可以理解为它是整个Store的对象.类似于this.$store,里面包含了state,getter,mutations,actions
       const res = await axios.get('/接口url')
       
       //这个时候就用到了 mutation_type.js
       context.commit( SET_USER_INFO,{userInfo:res.userData}
       )
    },
    
    //定义一个异步获取用户token的action
    async getToken(context){
         const res = await axios.get('/接口url')
         context.commit(
             SET_TOKEN,
             {token:res.token}
       )
    }
    
}

当然,我们可以通过解构的方式,将context对象里面的属性解构

async getUserInfo:({commit,dispatch})=>{
       const res = await axios.get('/接口url')
       commit( SET_USER_INFO, {userInfo:res.userData})
    }

想想一个实际开发场景,state里面的userInfo属性值是空的,当登录以后,再进行获取对应的信息。

登录以后,需要得到用户信息显示到界面上,那如何得到呢?

首先进入页面的时候调用actions中的getUserInfo方法

vue部分


<template>
    <div>{{realName}}</div>
</template>



export default{
    computed(){
    //第三步
    realName:this.$store.getters.realName
},


created(){ 
    //第一步
    this.reqUserInfo()
},
methods:{
    reqUserInfo(){
       //第二步使用action不想之前state、getters一样直接点调用,而是用dispatch关键字来派发action
        this.$store.dispatch('getUserInfo')
    }
}
    
}

来梳理一下上面的流程

  1. 页面初始化时调用 this.reqUserInfo()方法。 this.reqUserInfo()派发一个名为getUserInfo的action
  2. getUserInfo这个action中,我们执行了如下操作:
 async getUserInfo(context){
      //1.从接口里异步获取数据
      const res = await axios.get('/接口url')
      
      //2.将获取到的数据,通过commit提交一个mutation,去更改state里面的userInfo
      context.commit( SET_USER_INFO,{userInfo:res.userData})
   },
  1. 在computed中拿到最新的用户信息的用户名
//3.拿到最新的userInfo
  realName:this.$store.getters.realName

所以看到这里,大家应该明白Vuex所谓的单向数据流

界面——>派发action——>action提交mutation——>mutation更改state——>getters返回最新的state值到界面

4.1 mapActions辅助函数

mapActions和mapMutations一样,它将actions里面的方法映射到methods

所以,当action多了,我们可以这样使用

methods:{
    ...mapActions(['getUserInfo','getToken'])
}

相当于

methods:{
    getUserInfo(){
        return this.$store.dispatch(‘getUserInfo’)
    },
    getToken(){
        return this.$store.dispatch(‘getToken’)
    },
}

当然也可以有别名

methods:{
    ...mapActions(
      {reqUserData:'getUserInfo'},
      {reqToken:'getToken'},
    )
}

另外,补充一下

可以在action派发别的acton,如下

 async getUserInfo(context){
    
       const res = await axios.get('/接口url')
       context.commit( SET_USER_INFO,{userInfo:res.userData})
       
       //此处派发另外一个action
       context.dispatch('getToken') 
    },

5、总结一下

  • 依赖state得到新的数据,用this.$store.getters.值
  • 直接获取state的数据,用this.$store.state.值
  • 同步修改state的属性值,就用this.$store.commit('mutation值')
  • 异步修改state的属性值,就用this.$store.dispatch('action值')
  • mapState、mapMutations、mapGetters、mapActions等辅助函数,便于我们处理多个方法和属性