Vuex Actions 完全指南:从异步提交到组合编排的实战与源码解析
2026/9/19 6:51:30 网站建设 项目流程

Vuex Actions 完全指南:从异步提交到组合编排的实战与源码解析

【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex

Vuex 中,Action 是承载业务异步逻辑的核心层:它不直接修改 state,而是通过 commit 提交 mutation 来完成状态变更,因此可以安全地封装异步操作(如请求 API、定时任务)并组合多个操作。本文将以 Vuex 官方指南的 Actions 章节为骨架,结合当前仓库的 源码、store-util.js、helpers.js 及单元测试与示例,系统讲解 Action 的定义、派发(dispatch)、组件绑定、组合编排与命名空间行为,帮助你写出可维护、可测试的异步状态流。

什么是 Action:与 Mutation 的分工

在 Vuex 中,Action 与 Mutation 的功能定位截然不同:

  • Mutation 必须同步:它直接修改 state,任何异步操作都会破坏 Vuex 时间旅行调试的确定性;
  • Action 不修改 state,而是 commit mutation:Action 内部可以执行任意异步操作,等异步结果就绪后再通过commit提交对应 mutation 完成状态落地;
  • Action 可以包含任意异步操作:这是二者最本质的差异。

也就是说,业务逻辑中的副作用(网络请求、定时器、事件监听)都应放在 Action 中,让 Mutation 保持"纯粹地描述状态如何变化"。

注册一个最简单的 Action

const store = createStore({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })

Action 处理器接收一个context对象,该对象暴露了与 store 实例相同的一组方法/属性,因此你可以:

  • context.commit提交 mutation;
  • 通过context.statecontext.getters访问 state 与 getters;
  • context.dispatch调用其他 action。

关于context与 store 实例的关系,需要特别说明:context并不是 store 实例本身,而是一个"本地化上下文"。从源码 makeLocalContext 可以看出,contextdispatchcommitstategetters在命名空间模块中会被替换为绑定到该模块的本地版本(详见后文"命名空间下的 Action")。这也是官方文档在介绍 Modules 时专门提醒读者的原因。

使用 ES2015 参数解构简化代码

在实际开发中,我们往往只需要context中的某几个成员,尤其是需要多次调用commit时,可以借助 ES2015 参数解构(destructuring)直接抽取:

actions: { increment ({ commit }) { commit('increment') } }

解构出的commitcontext.commit完全等价,代码更简洁、意图更清晰。

派发 Action:store.dispatch

Action 通过store.dispatch方法触发:

store.dispatch('increment')

初看之下,这似乎有些多此一举:既然要增加 count,为什么不直接store.commit('increment')?关键区别在于——Mutation 必须同步,而 Action 不受此限制。Action 内部可以执行异步操作,例如 1 秒后再提交 mutation:

actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }

Payload 与对象式派发

Action 与 Mutation 一样支持两种派发形式——携带 payload 的常规调用,以及包含type字段的对象式调用:

// 携带 payload 派发 store.dispatch('incrementAsync', { amount: 10 }) // 对象式派发 store.dispatch({ type: 'incrementAsync', amount: 10 })

对象式派发的底层处理逻辑在 unifyObjectStyle 中:当第一个参数是对象且包含type字段时,会将其解构为{ type, payload, options },与普通调用统一处理;开发环境下若 type 不是字符串还会给出断言错误。

真实场景:购物车结算(checkout)

一个更贴近真实业务的例子是购物车结算 action,它同时涉及异步 API 调用多次 mutation 提交

actions: { checkout ({ commit, state }, products) { // 保存当前购物车中的商品 const savedCartItems = [...state.cart.added] // 发出结算请求,并乐观地清空购物车 commit(types.CHECKOUT_REQUEST) // shop API 接受成功回调和失败回调 shop.buyProducts( products, // 成功 () => commit(types.CHECKOUT_SUCCESS), // 失败:用保存的商品快照回滚 () => commit(types.CHECKOUT_FAILURE, savedCartItems) ) } }

这段代码演示了 Action 的核心设计:异步流程编排 + 副作用(状态变更)通过 commit 记录。成功与失败分别提交不同 mutation,失败时还能用之前保存的快照回滚状态。

当前仓库的 shopping-cart 示例 提供了一个完整的async/await版本实现,包含乐观清空购物车、异常回滚:

async checkout ({ commit, state }, products) { const savedCartItems = [...state.items] commit('setCheckoutStatus', null) // 先清空购物车 commit('setCartItems', { items: [] }) try { await shop.buyProducts(products) commit('setCheckoutStatus', 'successful') } catch (e) { console.error(e) commit('setCheckoutStatus', 'failed') // 请求失败则回滚到结算前的购物车 commit('setCartItems', { items: savedCartItems }) } }

在组件中派发 Action

方式一:直接通过this.$store.dispatch

安装 Vuex 插件后(见 store.js 的 install 方法,它会向app.config.globalProperties注入$store),组件内可以直接使用:

this.$store.dispatch('xxx')

方式二:mapActions辅助函数

mapActions将组件方法映射为store.dispatch调用(前提是根 store 已注入应用):

import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment', // 映射 `this.increment()` 为 `this.$store.dispatch('increment')` // mapActions 同样支持 payload: 'incrementBy' // 映射 `this.incrementBy(amount)` 为 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions({ add: 'increment' // 映射 `this.add()` 为 `this.$store.dispatch('increment')` }) } }

数组形式的成员直接以 action 名作为组件方法名;对象形式则允许为 action 起别名。

mapActions 的源码实现

从 helpers.js 的 mapActions 可以看到其底层原理:

export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} normalizeMap(actions).forEach(({ key, val }) => { res[key] = function mappedAction (...args) { // 从 store 获取 dispatch 函数 let dispatch = this.$store.dispatch if (namespace) { const module = getModuleByNamespace(this.$store, 'mapActions', namespace) if (!module) { return } dispatch = module.context.dispatch } return typeof val === 'function' ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this.$store, [val].concat(args)) } }) return res })

关键点:

  • normalizeNamespace负责处理命名空间参数:当第一个参数不是字符串时,视为未指定命名空间;否则自动在末尾补上/(见 normalizeNamespace);
  • 数组形式的val就是 action 名,映射后的方法等价于this.$store.dispatch(actionName, ...args)
  • 对象形式的值可以是函数,函数第一个参数接收dispatch,后续参数为组件调用时传入的参数,这让你能在映射时对 dispatch 做一层自定义包装(对应 API 文档 中mapActions的说明);
  • 若指定了命名空间但模块不存在,开发环境下会输出module namespace not found in mapActions()的错误。

mapActions的运行时行为也有对应的单元测试覆盖,可参考 helpers.spec.js。

组合 Action:处理复杂的异步流程

Action 通常是异步的,那么如何得知一个 action 执行完毕?更重要的是,如何把多个 action 组合起来编排更复杂的异步流程?

dispatch 返回 Promise

第一个要点:store.dispatch会处理被触发 action 处理器返回的 Promise,并且自身也返回 Promise

actions: { actionA ({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit('someMutation') resolve() }, 1000) }) } }

之后就可以这样等待它完成:

store.dispatch('actionA').then(() => { // ... })

在另一个 Action 中派发并等待

也可以在另一个 action 内部dispatch并串联后续逻辑:

actions: { // ... actionB ({ dispatch, commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) } }

使用 async / await 编排

如果配合 ES2017 的async / await(假设getData()getOtherData()均返回 Promise),代码会变得非常直观:

actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, commit }) { await dispatch('actionA') // 等待 `actionA` 完成 commit('gotOtherData', await getOtherData()) } }

actionB会先等待actionA完成,再获取gotOtherData并提交,形成清晰的串行依赖。

多模块同时响应 dispatch

值得注意:一次store.dispatch可能触发多个不同模块中的同名 action 处理器。这种情况下,返回的是一个 Promise,它会在所有被触发的处理器都 resolve 之后才 resolve

这在源码 store.js 的 dispatch 中体现得非常直接:

const result = entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry0

store._actions[type]是一个数组(registerAction采用entry.push的方式追加处理器,见 store-util.js),当同类型存在多个处理器时,dispatch 会用Promise.all并行等待所有处理器完成。这也意味着同名 action 在全局命名空间下会全部触发(详见 Modules 的命名空间说明)。

Action 返回值统一为 Promise

再看 registerAction 的包装逻辑:

let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload) if (!isPromise(res)) { res = Promise.resolve(res) }

两件事值得注意:

  1. 无论 handler 是否返回 Promise,最终都会被统一包装成 Promise——即使 handler 是同步的,dispatch 返回的也是 Promise,调用方可以统一用.then()/await处理;
  2. 传入 handler 的context是完整形态,包含dispatchcommitgettersstaterootGettersrootState六个成员(对应 API 文档 中 actions 的说明)——在根模块中staterootStategettersrootGetters是相同的;在模块中则分别指向模块本地与根级。

测试验证

当前仓库的单元测试完整覆盖了上述组合模式,可查阅 store.spec.js:

  • 同步派发(dispatching actions, sync);
  • 对象式派发(dispatching with object style);
  • 返回 Promise 的派发(dispatching actions, with returned Promise);
  • async/await组合多个 action(composing actions with async/await);
  • 捕获 action 内 Promise 错误(detecting action Promise errors)。

命名空间下的 Action

当模块启用了namespaced: true后,模块内的 action 类型会自动加上模块路径前缀。例如 modules.md 中展示的:

actions: { login () { ... } // -> dispatch('account/login') }

本地化的 dispatch 与 root 选项

命名空间模块中的 action 收到的是本地化dispatchcommit:不带前缀书写即可派发本模块的 action/提交本模块的 mutation。若需要派发全局命名空间的 action,可以传入{ root: true }作为第三个参数:

actions: { someAction ({ dispatch, commit, getters, rootGetters }) { getters.someGetter // -> 'foo/someGetter' rootGetters.someGetter // -> 'someGetter' dispatch('someOtherAction') // -> 'foo/someOtherAction' dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction' commit('someMutation') // -> 'foo/someMutation' commit('someMutation', null, { root: true }) // -> 'someMutation' } }

其底层实现位于 makeLocalContext:本地化的dispatch/commitoptions.root未设置时自动拼接命名空间前缀,并会在开发环境下对不存在的本地类型给出unknown local action type的报错提示。

在命名空间模块中注册全局 Action

如果希望在命名空间模块里注册全局action(不带前缀),可以把 action 定义为一个包含root: truehandler的对象:

{ actions: { someOtherAction ({dispatch}) { dispatch('someAction') } }, modules: { foo: { namespaced: true, actions: { someAction: { root: true, handler (namespacedContext, payload) { ... } // -> 'someAction' } } } } }

对应 installModule 中的注册逻辑:

module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) })

root: true的 action 直接以裸 key 注册到全局命名空间,handler仍是标准 action 函数(接收本地化 context),但通过dispatch('someAction')即可全局触发。

结合命名空间使用 mapActions

组件内绑定命名空间模块的 action 时,可把命名空间字符串作为mapActions的第一个参数:

methods: { ...mapActions('some/nested/module', [ 'foo', // -> this.foo() 'bar' // -> this.bar() ]) }

也可以使用createNamespacedHelpers预先绑定命名空间:

import { createNamespacedHelpers } from 'vuex' const { mapState, mapActions } = createNamespacedHelpers('some/nested/module') export default { methods: { // 在 `some/nested/module` 中查找 ...mapActions([ 'foo', 'bar' ]) } }

createNamespacedHelpers的实现非常轻量,只是把命名空间预绑定到各 helper 上(见 helpers.js)。

订阅 Action:subscribeAction

在 API 文档 中,store.subscribeAction用于监听 action 的派发,常被插件用于日志、埋点或调试:

const unsubscribe = store.subscribeAction((action, state) => { console.log(action.type) console.log(action.payload) })

它支持before/after/error三种钩子,分别对应 action 派发前、成功完成后、抛出错误时;返回的unsubscribe函数用于取消订阅。其内部实现在 store.js 与 store-util.js 的 genericSubscribe,dispatch的完整调用链会在before→ 执行 handler →after(或error)各阶段回调订阅者,这也是调试异步流程的利器。

小结

Action 是 Vuex 异步逻辑的总闸门,理解并善用它可以显著提升大型应用的状态可维护性。核心要点可归纳为:

  1. Action 不直接改 state,只通过commit提交 mutation,因此天然适合封装异步操作;
  2. store.dispatch始终返回 Promise,即使 handler 是同步的也会被包装,串行组合用async/await、并行组合依赖多模块同名 action 的Promise.all机制;
  3. 组件内优先使用mapActions(可配合命名空间参数或createNamespacedHelpers),避免样板代码;
  4. 命名空间模块中dispatch/commit默认本地化,需要访问全局资源时使用{ root: true }root: true的 action 声明;
  5. 通过 subscribeAction 可以精细观察 action 的完整生命周期,为插件化、调试与监控提供支撑。

深入阅读可继续查看 Modules(命名空间与模块内 action 的本地上下文)、Mutations(action 最终提交的同步状态变更)、Plugins(基于 action 订阅的插件开发),以及示例工程 shopping-cart 与 todomvc 中的真实 action 组织方式。

【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询