返回 导航

Vue.js

hangge.com

Vue.js - 使用Vuex进行状态管理详解5(actions执行异步操作)

作者:hangge | 2026-10-06 07:04
    在 Vuex 状态管理模式中,actions 是一个非常重要的概念。与 mutations 不同,actions 用于处理异步操作。下面我将详细介绍 actions 的基本使用、参数传递、辅助函数以及返回 Promise 对象等内容。

一、基本介绍

1,什么是 actions?

(1)actions 类似于 mutations,但是有以下两个关键区别:
  • Actions 不直接变更状态:而是通过提交 mutation 来间接改变状态
  • Actions 可以包含异步操作:而 mutations 必须是同步函数

(2)为什么要使用 actions?
  • 当需要执行异步操作(如 API 请求)后再更新状态时,必须使用 actions
  • mutations 必须是同步的,这样才能在 Vue DevTools 中追踪状态变化的前后快照
  • 如果在 mutation 中执行异步操作,将无法正确追踪状态变化
提示:Vuex 官方推荐的最佳实践是组件不应该直接提交 mutations,而是通过分发 actions 来间接提交 mutations。

2,actions 的基本结构

(1)一个 action 函数接收一个 context 对象作为参数,这个对象包含以下属性和方法:
  • context.commit:提交 mutation
  • context.dispatch:分发其他 action
  • context.state:获取 store 的状态
  • context.getters:获取 getters
  • context.rootState:获取根状态(在模块中使用)
  • context.rootGetters:获取根 getters(在模块中使用)

(2)context 参数支持 ES6 解构写法,这在实际开发中非常常用:
actions: {
  incrementAction ({ commit }) {
    commit('increment')
  }
}

二、actions 的基本使用

1,定义 actions

(1)下面我们通过一个计数器案例来演示 actions 的基本使用。首先修改 src/store/index.js 文件,在 actions 中添加两个 action 函数:
  • 在 incrementAction 函数中使用了 setTimeout 来模拟异步操作(如网络请求)。
  • 在 decrementAction 函数中展示了如何通过 ES6 解构获取 context 的各个属性。
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state) {
      state.counter++
    },
    decrement(state) {
      state.counter--
    }
  },
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    }
  },
  // actions 的基本使用
  actions: {
    incrementAction(context) {
      // 使用 setTimeout 模拟异步操作
      setTimeout(() => {
        context.commit('increment')  // 提交 mutation
      }, 1000)
    },
    decrementAction(context) {
      // ES6 解构 context 对象
      let { commit, dispatch, state, rootState, getters, rootGetters } = context
      commit('decrement')  // 提交 mutation
    }
  }
})

export default store

(2)注意:actions 中也可以提交多个 mutation:
actions: {
  incrementAction({ commit, dispatch }) {
    commit('increment')      // 提交第一个 mutation
    commit('anotherMutation') // 提交第二个 mutation
    dispatch('otherAction')   // 还可以分发其他 action
  }
}

2,在组件中分发 actions

(1)创建一个新的组件文件,通过两种方式分发 actions:
<template>
  <div>
    <h4>当前计数: {{ $store.state.counter }}</h4>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { useStore } from 'vuex'

export default {
  methods: {
    increment() {
      // Options API 语法:使用 this.$store.dispatch
      this.$store.dispatch('incrementAction')
    }
  },
  setup() {
    // Composition API 语法:使用 useStore
    const store = useStore()
    const decrement = () => {
      store.dispatch('decrementAction')
    }
    return { decrement }
  }
}
</script>

(2)运行效果如下:

三、actions 接收参数

1,payload 参数传递

(1)与 mutations 类似,actions 定义的函数也可以接收两个参数:第一个是 context,第二个是 payload(用于接收分发时传递的参数)。
提示:payload 可以是任何类型的数据,但通常建议使用对象形式,这样可以传递多个参数。

(2)下面我们为计数器添加一个 "+10" 的功能。首先在组件中添加按钮和分发逻辑:
<template>
  <div>
    <h4>当前计数: {{ $store.state.counter }}</h4>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="addTen">+10</button>
  </div>
</template>

<script>
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    const increment = () => {
      store.dispatch('incrementAction')
    }

    const decrement = () => {
      store.dispatch('decrementAction')
    }

    const addTen = () => {
      // 分发 action,并传递对象参数
      store.dispatch('incrementNAction', { num: 10 })
    }

    return { increment, decrement, addTen }
  }
}
</script>

(3)然后在 store 中定义对应的 action 和 mutation:
import { createStore } from 'vuex'

// 常量形式的 mutation 类型
const INCREMENT_N = 'INCREMENT_N'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state) {
      state.counter++
    },
    decrement(state) {
      state.counter--
    },
    INCREMENT_N(state, payload) {
      state.counter += payload.num
    }
  },
  actions: {
    incrementNAction(context, payload) {
      // payload 接收 dispatch 传递过来的参数
      // payload 值为 { num: 10 }
      context.commit(INCREMENT_N, payload)
    }
  }
})

export default store

(4)运行效果如下:

2,对象风格分发

store.dispatch 还支持对象风格的分发语法:
提示:使用对象风格分发时,整个对象会作为 payload 传递给 action 函数。
// 普通分发方式
store.dispatch('incrementNAction', { num: 10 })

// 上面的写法等同于下面的对象风格写法
store.dispatch({
  type: 'incrementNAction',
  num: 10
})

四、mapActions 辅助函数

1,什么是 mapActions?

(1)Vuex 提供了 mapActions 辅助函数,用于将组件的 methods 映射为 store.dispatch 调用,从而简化代码编写。
(2)mapActions 支持两种映射方式:
  • 数组方式:直接映射同名方法
  • 对象方式:可以重命名映射的方法名

2,Options API 中使用 mapActions

在 Options API 中,mapActions 直接映射到 methods 选项:
提示:使用对象方式时,如果需要传递参数,可以在模板中直接调用时传入,如 addTen({ num: 20 })。
<template>
  <div>
    <h4>当前计数: {{ $store.state.counter }}</h4>
    <!-- 点击按钮会调用映射好的 action -->
    <button @click="incrementAction">+1</button>
    <button @click="decrement">-1</button>
    <button @click="addTen({ num: 20 })">+20</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    // 数组方式:将 this.incrementAction() 映射为 this.$store.dispatch('incrementAction')
    ...mapActions(['incrementAction']),

    // 对象方式:可以重命名
    ...mapActions({
      decrement: 'decrementAction',     // 将 this.decrement() 映射为 dispatch('decrementAction')
      addTen: 'incrementNAction' // 将this.addTen(payload)映射为dispatch('incrementNAction', payload)
    })
  }
}
</script>

3,Composition API 中使用 mapActions

(1)在 Composition API 的 setup 函数中,也可以使用 mapActions:
<script>
import { mapActions, useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    // mapActions 返回一个对象,包含映射好的方法
    const actionsFuncs = mapActions({
      decrement: 'decrementAction',
      addTen: 'incrementNAction'
    })

    // 将映射好的方法解构返回
    return {
      incrementAction: () => store.dispatch('incrementAction'),
      ...actionsFuncs
    }
  }
}
</script>

(2)更推荐的方式是直接使用 useStore:
<script setup>
import { useStore } from 'vuex'

const store = useStore()

// 直接定义分发 action 的方法
const incrementAction = () => store.dispatch('incrementAction')
const decrementAction = () => store.dispatch('decrementAction')
const incrementNAction = (payload) => store.dispatch('incrementNAction', payload)
</script>

五、action 返回 Promise 对象

1,为什么需要返回 Promise?

(1)action 通常用于处理异步操作。如果我们想知道 action何时执行完成,可以让 action 函数返回一个 Promise 对象。

(2)实际上,store.dispatch 默认就会返回 action 函数返回的 Promise,这样我们就可以在分发后使用 .then() 或 async/await 来处理异步结果。

2,实战案例:获取网络数据

(1)下面通过一个获取 UUID 的案例来演示如何让 action 返回 Promise。首先修改 store 定义:
提示:https://httpbin.org 是一个测试 HTTP 请求和响应的免费服务,非常适合用于学习和测试。
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      uuid: null
    }
  },
  mutations: {
    addUUID(state, payload) {
      state.uuid = payload  // 存储网络获取到的 uuid
    }
  },
  getters: {
    // getters 定义...
  },
  actions: {
    // 编写一个获取 uuid 的 action
    getUUIDAction({ commit }) {
      // 直接返回 Promise 对象,作为 dispatch 函数的返回值
      return new Promise((resolve, reject) => {
        // 发起网络请求(这是一个免费获取 uuid 的接口)
        fetch('https://httpbin.org/uuid')
          // 将响应解析为 JSON 格式
          .then(res => res.json())
          .then(data => {
            // 将请求获取的数据存到 Vuex 中
            commit('addUUID', data.uuid)
            // 调用 resolve 完成异步操作
            resolve(data)
          })
          .catch(err => {
            // 错误处理
            reject(err)
          })
      })
    }
  }
})

export default store

(2)在组件中使用这个 action:
<template>
  <div>
    <h4>uuid: {{ $store.state.uuid }}</h4>
  </div>
</template>

<script>
import { onMounted } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    onMounted(() => {
      // 分发 action,返回一个 Promise 对象
      const promise = store.dispatch('getUUIDAction')

      // 使用 then 处理异步请求成功的结果
      promise.then(res => {
        console.log('请求成功:', res)
      }).catch(err => {
        console.log('请求失败:', err)
      })
    })
  }
}
</script>

(3)运行效果:


3,使用 async/await 简化代码

(1)我们可以使用 async/await 语法让代码更加简洁清晰。首先修改 store 定义:
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      uuid: null,
    }
  },
  mutations: {
    addUUID(state, payload) {
      state.uuid = payload // 存储网络获取到的 uuid
    },
  },
  getters: {
    // getters 定义...
  },
  actions: {
    // 编写一个获取 uuid 的 action
    // 使用 async/await 语法
    async getUUIDAction({ commit }) {
      try {
        // 发起网络请求
        const response = await fetch('https://httpbin.org/uuid')
        const data = await response.json()

        // 提交 mutation 更新状态
        commit('addUUID', data.uuid)

        // 返回数据
        return data
      } catch (err) {
        // 错误处理
        console.error('获取 UUID 失败:', err)
        throw err
      }
    },
  },
})

export default store

(2)在组件中使用:
<template>
  <div>
    <h4>uuid: {{ $store.state.uuid }}</h4>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { useStore } from 'vuex'

const store = useStore()

onMounted(async () => {
  try {
    // 使用 await 等待 action 完成
    const result = await store.dispatch('getUUIDAction')
    console.log('请求成功:', result)
  } catch (err) {
    console.log('请求失败:', err)
  }
})
</script>

4,组合多个 actions

(1)有时候我们需要在一个 action 中调用另一个 action,并等待其完成。这可以通过返回 Promise 来实现:
actions: {
  actionA({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  },

  actionB({ dispatch, commit }) {
    // 先等待 actionA 完成,再执行其他操作
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

(2)使用 async/await 可以让组合 actions 更加直观:
提示:一个 store.dispatch 可以在不同模块中触发多个 action 函数。在这种情况下,返回的 Promise 只有当所有触发的函数都完成后才会完成。
actions: {
  async actionA({ commit }) {
    commit('gotData', await getData())
  },

  async actionB({ dispatch, commit }) {
    // 等待 actionA 完成
    await dispatch('actionA')
    // 然后执行其他操作
    commit('gotOtherData', await getOtherData())
  }
}

附:最佳实践与常见问题

1,actions 与 mutations 的职责划分

遵循以下职责划分原则可以让代码更加清晰:
  • Mutations:只负责同步更新状态,每次提交只做一件事
  • Actions:负责异步操作、提交 mutations、分发其他 actions
  • 组件:只分发 actions,不直接提交 mutations
注意:不要在 mutations 中执行异步操作。这会导致 Vue DevTools 无法正确追踪状态变化,也会使调试变得困难。

2,常量形式的 mutation 类型

(1)在大型项目中,建议使用常量代替 mutation 类型字符串,这样可以利用 ESLint 等工具进行代码检查,同时让代码更清晰:
// mutation-types.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
export const INCREMENT_N = 'INCREMENT_N'

(2)在 store 中使用:
import { INCREMENT, DECREMENT } from './mutation-types'

const store = createStore({
  mutations: {
    [INCREMENT](state) {
      state.counter++
    },
    [DECREMENT](state) {
      state.counter--
    }
  },
  actions: {
    incrementAction({ commit }) {
      commit(INCREMENT)
    }
  }
})

3,常见的错误处理方式

(1)在 action 中进行网络请求时,务必做好错误处理:
actions: {
  async fetchDataAction({ commit }) {
    try {
      const response = await fetch('/api/data')
      if (!response.ok) {
        throw new Error('请求失败')
      }
      const data = await response.json()
      commit('setData', data)
      return data
    } catch (err) {
      // 可以提交一个错误状态的 mutation
      commit('setError', err.message)
      throw err  // 继续抛出错误,让组件也能处理
    }
  }
}

(2)在组件中处理错误:
<script setup>
import { useStore } from 'vuex'

const store = useStore()

const loadData = async () => {
  try {
    await store.dispatch('fetchDataAction')
  } catch (err) {
    // 在组件中处理错误,如显示提示信息
    alert('加载失败: ' + err.message)
  }
}
</script>
评论

全部评论(0)

回到顶部