
Vuex Actions 详解异步逻辑编排、Promise 组合与源码级实现解析【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex本篇围绕 Vuex 的 Actions动作机制展开从「动作与变化的本质区别」讲起覆盖 action 注册、context 对象、store.dispatch的 payload/对象两种派发风格、组件内mapActions辅助函数以及用 Promise 与 async/await 组合多个异步动作的完整方案。结合 src/store.js 与 src/store-util.js 中的真实实现帮助读者理解「为什么 context 不是 store 实例」「dispatch 返回的 Promise 从何而来」最终能独立写出可复用的异步业务流程如下单结算。Vuex 官方示例中展示的单向前端数据流State → Getters / Mutations / Actions → ViewsActions 与 Mutations 的核心区别在 Vuex 中Actions动作与 Mutations变化形似但职责不同官方指南docs/ptbr/guide/actions.md给出的定义是动作不直接改变状态而是提交commitmutations动作可以包含任意异步操作。这正是 Vuex「mutation 必须同步、副作用必须可追踪」设计原则的延伸状态变更本身保持同步且可被 DevTools 记录而网络请求、定时器等异步流程则被收拢到 action 这一层。一个最简单的动作注册示例const store createStore({ state: { count: 0 }, mutations: { increment (state) { state.count } }, actions: { increment (context) { context.commit(increment) } } })Context 对象动作的「局部 store」动作处理函数收到的第一个参数是一个context 对象它暴露与 store 实例上相同的一组方法/属性context.commit—— 提交 mutationcontext.state/context.getters—— 访问状态与 gettercontext.dispatch—— 调用其他动作。实践中常借助 ES2015 参数解构简化代码尤其在需要多次commit时actions: { increment ({ commit }) { commit(increment) } }为什么 context 不是 store 实例本身这一点在引入 Modules模块 后才显得关键。从源码看每个模块安装时都会通过makeLocalContext生成一个局部上下文定义在 src/store-util.js根级无命名空间模块的local.dispatch/local.commit直接就是store.dispatch/store.commit命名空间模块的local.dispatch会在派发前自动拼接namespace type除非显式传入{ root: true }选项local.state是一个 getter通过getNestedState(store.state, path)动态取到模块自己的 state 切片命名空间模块的local.getters则经过makeLocalGetters代理只暴露本命名空间下的 getter。因此「context 不是 store 实例」是为了让命名空间模块的动作拿到的是模块作用域内的dispatch/commit/state/getters。对于根级 storecontext 中的state/getters与 store 上的等价但同时还提供了rootState/rootGetters这一点在 types/index.d.ts 的类型定义中体现得很清楚export interface ActionContextS, R { dispatch: Dispatch; commit: Commit; state: S; getters: any; rootState: R; rootGetters: any; }动作真正被注册、包装的位置在 src/store-util.js 的registerActionfunction registerAction (store, type, handler, local) { const entry store._actions[type] || (store._actions[type] []) entry.push(function wrappedActionHandler (payload) { 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) } // ... }) }两个值得注意的实现细节同一个 type 可以注册多个 handler——_actions[type]是一个数组。这为后文「跨模块同名动作」的合并派发埋下伏笔返回值统一被包装成 Promise——即使动作本身是同步的dispatch拿到的也是可.then的 Promise。派发Dispatching动作动作通过store.dispatch触发store.dispatch(increment)底层实现dispatch 的完整调用链Store.dispatch的实现位于 src/store.js其流程为归一化参数先调用unifyObjectStyle(_type, _payload)见 src/store-util.js判断第一个参数是否为带type字段的对象从而统一得到{ type, payload }未知动作告警若this._actions[type]不存在开发环境下输出[vuex] unknown action type: ${type}并直接返回before 订阅者遍历this._actionSubscribers中带有before的回调由store.subscribeAction注册在动作执行前被调用执行 handlerconst result entry.length 1 ? Promise.all(entry.map(handler handler(payload))) : entry0即当一个store.dispatch在不同模块中触发了多个同名 handler 时返回的 Promise 会在它们全部 resolve 后才 resolve——这与官方文档末尾的提示完全一致 5.after / error 订阅者 统一返回 Promise外层再包一层new Promise在结果 resolve 时通知after订阅者在 reject 时通知error订阅者最终resolve(res)或reject(error)。所以从源码结构看store.dispatch的返回值永远是一个 Promise即使动作没有返回值registerAction里的Promise.resolve(res)也会兜底。这使得.then()/await写法在任何动作上都成立。Payload 与对象风格派发动作支持两种等价写法均可复制运行// 派发带 payload store.dispatch(incrementAsync, { amount: 10 }) // 对象风格派发 store.dispatch({ type: incrementAsync, amount: 10 })在 TypeScript 侧这两种重载都体现在 types/index.d.ts 的Dispatch接口中export interface Dispatch { (type: string, payload?: any, options?: DispatchOptions): Promiseany; P extends Payload(payloadWithType: P, options?: DispatchOptions): Promiseany; }单元测试 test/unit/store.spec.js 分别以「dispatching actions, sync」与「dispatching with object style」两个用例验证了这两种派发方式最终都会把 payload 正确传给动作并触发 mutation。为什么需要 dispatch 而不是直接 commit乍看之下只想加一计数时直接store.commit(increment)似乎更直接。区别在于mutation 必须同步而动作不必。动作内部可以执行任意异步操作actions: { incrementAsync ({ commit }) { setTimeout(() { commit(increment) }, 1000) } }实战示例购物车结算官方文档给出的更贴近真实业务的动作是「购物车结算」——它调用一个异步 API并提交多个 mutationsactions: { checkout ({ commit, state }, products) { // 保存当前购物车中的商品 const savedCartItems [...state.cart.added] // 发送结算请求并乐观地清空购物车 commit(types.CHECKOUT_REQUEST) // 商店 API 接受成功与失败两个回调 shop.buyProducts( products, // 成功回调 () commit(types.CHECKOUT_SUCCESS), // 失败回调 () commit(types.CHECKOUT_FAILURE, savedCartItems) ) } }要点在于整个流程是一个异步操作序列而所有副作用状态变化都通过 commit 落盘。仓库自带的示例 examples/classic/shopping-cart/store/modules/cart.js 给出了该场景的现代 async/await 版本并演示了失败时的购物车回滚actions: { 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 }) } } }对照官方文档的回调版写法可以看出无论用回调还是try/catch「先保存现场 → 乐观提交 → 按结果分支提交」的编排模式是一致的。在组件中派发动作组件中有两种派发方式直接调用this.$store.dispatch(xxx)使用辅助函数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) }) } }mapActions 源码解析mapActions位于 src/helpers.js核心逻辑是res[key] function mappedAction (...args) { 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)) }由此可以确认几种写法的真实行为数组形式mapActions([increment])生成的方法把全部实参透传给this.$store.dispatch(increment, ...args)所以this.incrementBy(amount)等价于dispatch(incrementBy, amount)对象形式mapActions({ add: increment })方法名add映射到动作类型increment函数形式映射项本身是函数时以dispatch作为第一个参数调用可自定义派发逻辑这一点由 test/unit/helpers.spec.js 的mapActions (function)用例验证命名空间外层再包一层normalizeNamespacemapActions(foo/, {...})会把 dispatch 替换为module.context.dispatch即自动带上foo/前缀见 test/unit/helpers.spec.js 的命名空间用例。在 Composition API 中则不需要mapActions直接在setup里暴露派发函数即可例如 docs/ptbr/guide/composition-api.md 中的asyncIncrement: () store.dispatch(asyncIncrement)。动作的组合Composing Actions动作通常是异步的如何知道一个动作何时完成如何把多个动作组合起来处理更复杂的异步流程基于 Promise 的组合store.dispatch能处理被触发动作 handler 返回的 Promise因此动作可以直接return new Promiseactions: { actionA ({ commit }) { return new Promise((resolve, reject) { setTimeout(() { commit(someMutation) resolve() }, 1000) }) } }外部即可链式等待store.dispatch(actionA).then(() { // ... })也可以在另一个动作内部组合actions: { // ... actionB ({ dispatch, commit }) { return dispatch(actionA).then(() { commit(someOtherMutation) }) } }基于 async/await 的组合使用 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()) } }test/unit/store.spec.js 的「composing actions with async/await」用例正是按此模式测试two动作先await dispatch(TEST, 1)断言中间状态再提交自己的 mutation最终状态值正确。多 handler 派发与错误传播两个容易踩坑的点均能在源码与测试中找到依据跨模块同名动作由于_actions[type]是数组entry.length 1时返回Promise.all(...)src/store.js即 Promise 在所有被触发的 handler 都 resolve 后才 resolve。错误会向外传播且通知 DevToolsregisterAction中若 store 挂载了_devtoolHookhandler 返回的 Promise 会额外接一个.catch先emit(vuex:error, err)再throw errsrc/store-util.js。test/unit/store.spec.js 的「detecting action Promise errors」用例验证了动作 reject 后store.dispatch返回的 Promise 会被 reject、thenSpy不会被调用且 devtoolHook 收到vuex:error事件。这意味着动作内部抛错/ reject 时调用方应该用try/catch或.catch()兜底否则会产生未处理的 Promise 拒绝。小结回到官方文档的主线Actions 在 Vuex 中的定位可以归纳为三层职责分离mutation 保持同步与可追踪action 承载一切异步流程状态副作用一律通过commit落盘见 Mutations 指南派发体系store.dispatch支持 payload 与对象两种风格始终返回 PromisemapActions与命名空间机制让组件层的调用保持简洁src/helpers.js组合能力Promise 链与 async/await 让多个异步动作可以串行/并行编排Promise.all语义与 error 订阅者共同保证了多模块场景下的可观测性src/store.js。理解 context 由makeLocalContext生成的机制后读者再学习 Modules模块 中的命名空间与{ root: true }选项时就不会觉得它们突兀——它们本质上都是这套「局部上下文」机制的自然延伸。【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考