Vue3学习 --- vuex基本使用(下)

2,365 阅读5分钟

actions

Action类似于mutation,不同的是

mutation中不推荐放置异步请求,而action中可以存放异步请求

在action中,我们获取到异步请求得到的数据后,需要通过commit将数据传递给mutation,

让mutation来帮助我们将数据存储到state中

基本使用

store.js

actions: {
   // 参数1:
   // context是一个和store实例类似的对象
   // context上有属性 state, rootState来获取到state中的数据
   //   ----- 如果没有分module的时候, rootState的值和state的值是一致的
   // context上还有方法:
   //  1. actions --- 可以在一个action事件中去调用另一个事件
   //  2. getters和rootGetters --- 调用getters中的方法
   //   ----- 如果没有分module的时候, getters中的值和rootGetterd的值是一致的
   //  3. mutations --- 调用mutations中的方法

   // 参数2:
   //  传入的参数 --- 如果调用者没有传入任何的参数,那么payload对应的就是事件对象
   incrementNAction(context, payload) {
     // 使用setTimeout来模拟异步请求  
     setTimeout(() => context.commit('incrementN', payload), 1000)
   }
 }

调用者

methods: {
  incrementN() {
    // 通过dispath方法,来分发一个actions
    // 参数1: 事件名
    // 参数2: 参数
    //   --- 如果参数有多个,需要将参数整合为一个对象后再进行传递
    //   --- 没有参数的时候,第二个参数可以不传
    this.$store.dispatch('incrementNAction', {
      step: 10
    })
  }
}
methods: {
  incrementN() {
    // dispatch和mutation一样,同样支持以对象的形式进行事件和参数的传递
    this.$store.dispatch({
      type: 'incrementNAction',
      step: 10
    })
  }
}

异步监听

action中存放着的一般是异步请求,所以某些时候,我们需要知道 action中的异步请求什么时候结束

此时我们可以将action中的异步请求封装为一个promise进行返回

store.js

actions: {
  incrementAction({ commit }) {
    return new Promise((resolve, reject) => {
      try {
        setTimeout(() => {
          commit('increment')
          resolve()
        }, 1000)
      } catch (e) {
        reject(e)
      }
    })
  }
}

调用者

async incrementInAction() {
  try {
    await this.incrementAction()
    console.log('action执行完毕了')
  } catch(e) {
    console.error('action执行失败了', e);
  }
}

mapActions

action也有对应的辅助函数, 其和mapMutations比较类似

数组写法

...mapActions(['incrementNAction', 'decrementNAction'])

对象语法

...mapActions({
  incrementNAction: 'incrementNAction',
  decrementNAction: 'decrementNAction'
})

module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,

当应用变得非常复杂时,store 对象就有可能变得相当臃肿

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)

每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块 (即module自己的module);

homeModule.js --- 模块

// 模块的本质就是一个对象
const home = {
  state() {
    return {
      msg: 'Home'
    }
  }
}

export default home

store.js --- 汇聚模块

import { createStore } from 'vuex'
import home from './modules/homeModule'

export default createStore({
  state() {
    return {
      msg: 'root'
    }
  },

  modules: {
    // key是实际需要使用的名字
    // value是引入的模块对象
    home
  }
})

使用者

<template>
  <div>
    <h2>{{ $store.state.msg }}</h2>
    <!-- 模块中state的使用 --- 模块中的state会被合并到根state中 -->
    <h2>{{ $store.state.home.msg }}</h2>
  </div>
</template>

命名空间

默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的:

  • 这样使得多个模块能够对同一个 actions 或 mutations作出响应
    • actions 是异步的所以会被加入队列中,并依次执行
    • mutations 会同名覆盖
    • getters如果出现同名,会直接报错

homeModule.js

const home = {
  state() {
    return {
      count: 0
    }
  },

  mutations: {
    increment(state) {
      state.count++
    }
  }
}

export default home

store.js

import { createStore } from 'vuex'
import home from './modules/homeModule'

export default createStore({
  state() {
    return {
      count: 0
    }
  },

  mutations: {
    increment(state) {
      state.count++
    }
  },

  modules: {
    home
  }
})

使用者

<template>
  <div>
    <h2>root count: {{ $store.state.count }}</h2>
    <!-- $store.state.home取到的实际上是 home模块的state函数返回的对象 -->
    <h2>home count: {{ $store.state.home.count }}</h2>
    <button @click="() => $store.commit('increment')">+1</button>
  </div>
</template>

此时,点击按钮的时候会发现,子模块和根模块的count都发生了改变,

因为默认情况下,commit是无法和state一样,准确区分模块的

这同样出现在mutations和 actions上

<!-- 
   默认情况下,我们无法准确区分不同模块的 actions 
   vuex默认将所有模块中的actions进行了合并
-->
<button @click="() => $store.dispatch('incrementAction')">+1(action)</button>

如果我们希望模块具有更高的封装度和复用性或者可以区分不同模块中对应的方法的时候,我们可以开启模块的命名空间

当模块开启命名空间后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

<h2>root count: {{ $store.state.count }}</h2>
<h2>home count: {{ $store.state.home.count }}</h2>

<!-- 
	需要注意的是,gettets在传递路径标识的时候,
	是将路径标识作为getters对象的属性来进行获取
  也就是使用 模块名/ 来作为前缀以作区分
-->
<h2>home getters count: {{ $store.getters['home/doubleCount'] }}</h2>

<!-- commit和dispatch的路径标识,是作为函数的参数进行传递 -->
<button @click="() => $store.commit('home/increment')">+1(mutations)</button>
<button @click="() => $store.dispatch('home/incrementAction')">+1(actions)</button>

参数说明

getters: {
  // 在开启了命名空间后,子模块中的getters方法比原本的getters方法会多2个参数
  
  /*
    roottState --- 使用根store的state对象的属性
    rootGetters --- 调用根store的getters方法
  */
  doubleCount(state, getters, rootState, rootGetters) {
    return state.count * 2
  }
}
actions: {
  // action无论是否开启了模块,传入的context都有6个属性可以使用

  // 如果开启了模块 那么
  //    commit, state, dispatch, getters使用的就是模块中对应的commit, state, dispatch, getters
  //    rootState, rootGetters就是根store的state和getters

  // 如果没有开启模块
  //    commit, state, dispatch, getters使用的就是模块中对应的commit, state, dispatch, getters
  //    rootState, rootGetters就是当前store的state和getters
  //    也就是说此时state和rootState的值,以及getters和rootGetters的值是一样的
  incrementAction({ commit, state, dispatch, getters, rootState, rootGetters }) {
    setTimeout(() => ctx.commit('increment'), 1000)
  }
}
actions: {
  incrementAction({ commit, dispatch }) {
    // 执行自己的commit方法
    setTimeout(() => commit('increment'), 1000)

    // 当在子模块中设置{root: true}的时候,就代表着操作的是根store的mutations或actions
    // 参数一: 事件名
    // 参数二: 需要传入的参数 没有的时候需要使用null来占位

    // 参数三: 省略表示操作的是当前store,
    //        设置了{root: true}表示当前需要执行的mutations或actions
    //        是根store中的对应mutations或actions
    commit('increment', null, {root: true})
    dispatch('incrementAction', null, {root: true})
  }
}

辅助函数

vue3为我们提供了mapState,mapGetters, mapMutation,mapAction这4个辅助函数

但是这4个辅助函数,默认情况下,只能处理root state中的状态,

如果需要让这4个辅助函数可以处理模块中的状态,我们需要对其进行对应的封装

options api

方法一

<template>
  <div>
    <h2>count: {{ count }}</h2>
    <h2>doubleCount: {{ doubleCount }}</h2>

    <button @click="increment">increment</button>
    <button @click="incrementAction">incrementAction</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'App',

  computed: {
    ...mapState({
      count: state => state.home.count
    }),
    ...mapGetters({
      doubleCount: 'home/doubleCount'
    })
  },

  methods: {
    ...mapMutations({
      increment: 'home/increment'
    }),
    ...mapActions({
      incrementAction: 'home/incrementAction'
    })
  }
}
</script>

方式二

<template>
  <div>
    <h2>count: {{ count }}</h2>
    <h2>doubleCount: {{ doubleCount }}</h2>

    <button @click="increment">increment</button>
    <button @click="incrementAction">incrementAction</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'App',

  computed: {
    // 当然这些辅助函数的第二个参数,也是可以写成对象形式的
    ...mapState('home', ['count']),
    ...mapGetters('home', ['doubleCount'])
  },

  methods: {
    ...mapMutations('home', ['increment']),
    ...mapActions('home', ['incrementAction'])
  }
}
</script>

方式三

<template>
  <div>
    <h2>count: {{ count }}</h2>
    <h2>doubleCount: {{ doubleCount }}</h2>

    <button @click="increment">increment</button>
    <button @click="incrementAction">incrementAction</button>
  </div>
</template>

<script>
// 通过这个helper函数,可以将生成属于模块的辅助函数
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers('home')

export default {
  name: 'App',

  computed: {
    // 当然这些辅助函数的参数,也是可以写成对象形式的
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },

  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['incrementAction'])
  }
}
</script>

composition api

useStateuseGetters这两个辅助函数是不能再composition api中直接使用的,需要对他们对应的内容进行二次封装

useMapper.js

import { useStore } from 'vuex'
import { computed } from 'vue'

export default function(mapper, mapFn) {
    const store = useStore()
    const storeState = {}

    const mapStateFns = mapFn(mapper)

    Object.keys(mapStateFns).forEach(key => {
      storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
    })

    return storeState
}

useGetters.js

import useMapper from './useMapper'
import { mapGetters, createNamespacedHelpers } from 'vuex'

export default function(nameOrMapper, mapper) {
  let mapperFn = mapGetters
  let mapperObj = mapper

  if (typeof nameOrMapper === 'string' && nameOrMapper.length) {
    mapperFn = createNamespacedHelpers(nameOrMapper).mapGetters
  } else {
    mapperObj = nameOrMapper
  }

  return useMapper(mapperObj, mapperFn)
}

useState.js

import useMapper from './useMapper'
import { mapState, createNamespacedHelpers } from 'vuex'

export default function(nameOrMapper, mapper) {
  let mapperFn = mapState
  let mapperObj = mapper

  if (typeof nameOrMapper === 'string' && nameOrMapper.length) {
    mapperFn = createNamespacedHelpers(nameOrMapper).mapState
  } else {
    mapperObj = nameOrMapper
  }

  return useMapper(mapperObj, mapperFn)
}

index.js

import useGetters from './useGetters'
import useState from './useState'

export {
  useGetters,
  useState
}

使用者

<template>
  <div>
    <h2>count: {{ count }}</h2>
    <h2>doubleCount: {{ doubleCount }}</h2>

    <button @click="increment">increment</button>
    <button @click="incrementAction">incrementAction</button>
  </div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
import { useState, useGetters } from './hooks'

// 注意: createNamespacedHelpers提供的辅助函数内部依旧是使用this.$store.xxx的方式去获取对应的store值
// 所以如果是在script setup中使用的时候,需要自己手动将this绑定为类似于之前的{$store: store}这类对象
// 以便于createNamespacedHelpers提供的辅助函数内部可以获取正确的state值
const { mapActions, mapMutations } = createNamespacedHelpers('home')

export default {
  name: 'App',

  setup() {
    return {
      ...useState('home', ['count']),
      ...useGetters('home', ['doubleCount']),
      ...mapActions(['incrementAction']),
      ...mapMutations(['increment'])
    }
  }
}
</script>
</script>