
Vuex Actions 完全指南从异步提交到组合编排的实战与源码解析【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuexVuex 中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 mutationAction 内部可以执行任意异步操作等异步结果就绪后再通过commit提交对应 mutation 完成状态落地Action 可以包含任意异步操作这是二者最本质的差异。也就是说业务逻辑中的副作用网络请求、定时器、事件监听都应放在 Action 中让 Mutation 保持纯粹地描述状态如何变化。注册一个最简单的 Actionconst store createStore({ state: { count: 0 }, mutations: { increment (state) { state.count } }, actions: { increment (context) { context.commit(increment) } } })Action 处理器接收一个context对象该对象暴露了与 store 实例相同的一组方法/属性因此你可以用context.commit提交 mutation通过context.state和context.getters访问 state 与 getters用context.dispatch调用其他 action。关于context与 store 实例的关系需要特别说明context并不是 store 实例本身而是一个本地化上下文。从源码 makeLocalContext 可以看出context的dispatch、commit、state、getters在命名空间模块中会被替换为绑定到该模块的本地版本详见后文命名空间下的 Action。这也是官方文档在介绍 Modules 时专门提醒读者的原因。使用 ES2015 参数解构简化代码在实际开发中我们往往只需要context中的某几个成员尤其是需要多次调用commit时可以借助 ES2015 参数解构destructuring直接抽取actions: { increment ({ commit }) { commit(increment) } }解构出的commit与context.commit完全等价代码更简洁、意图更清晰。派发 Actionstore.dispatchAction 通过store.dispatch方法触发store.dispatch(increment)初看之下这似乎有些多此一举既然要增加 count为什么不直接store.commit(increment)关键区别在于——Mutation 必须同步而 Action 不受此限制。Action 内部可以执行异步操作例如 1 秒后再提交 mutationactions: { 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并且自身也返回 Promiseactions: { 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))) : entry0store._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) }两件事值得注意无论 handler 是否返回 Promise最终都会被统一包装成 Promise——即使 handler 是同步的dispatch 返回的也是 Promise调用方可以统一用.then()/await处理传入 handler 的context是完整形态包含dispatch、commit、getters、state、rootGetters、rootState六个成员对应 API 文档 中 actions 的说明——在根模块中state与rootState、getters与rootGetters是相同的在模块中则分别指向模块本地与根级。测试验证当前仓库的单元测试完整覆盖了上述组合模式可查阅 store.spec.js同步派发dispatching actions, sync对象式派发dispatching with object style返回 Promise 的派发dispatching actions, with returned Promise用async/await组合多个 actioncomposing actions with async/await捕获 action 内 Promise 错误detecting action Promise errors。命名空间下的 Action当模块启用了namespaced: true后模块内的 action 类型会自动加上模块路径前缀。例如 modules.md 中展示的actions: { login () { ... } // - dispatch(account/login) }本地化的 dispatch 与 root 选项命名空间模块中的 action 收到的是本地化的dispatch与commit不带前缀书写即可派发本模块的 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/commit在options.root未设置时自动拼接命名空间前缀并会在开发环境下对不存在的本地类型给出unknown local action type的报错提示。在命名空间模块中注册全局 Action如果希望在命名空间模块里注册全局action不带前缀可以把 action 定义为一个包含root: true与handler的对象{ 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。订阅 ActionsubscribeAction在 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 的 genericSubscribedispatch的完整调用链会在before→ 执行 handler →after或error各阶段回调订阅者这也是调试异步流程的利器。小结Action 是 Vuex 异步逻辑的总闸门理解并善用它可以显著提升大型应用的状态可维护性。核心要点可归纳为Action 不直接改 state只通过commit提交 mutation因此天然适合封装异步操作store.dispatch始终返回 Promise即使 handler 是同步的也会被包装串行组合用async/await、并行组合依赖多模块同名 action 的Promise.all机制组件内优先使用mapActions可配合命名空间参数或createNamespacedHelpers避免样板代码命名空间模块中dispatch/commit默认本地化需要访问全局资源时使用{ root: true }或root: true的 action 声明通过 subscribeAction 可以精细观察 action 的完整生命周期为插件化、调试与监控提供支撑。深入阅读可继续查看 Modules命名空间与模块内 action 的本地上下文、Mutationsaction 最终提交的同步状态变更、Plugins基于 action 订阅的插件开发以及示例工程 shopping-cart 与 todomvc 中的真实 action 组织方式。【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考