6.1 Vuex 核心概念:四件套与单行道


文档摘要

6.1 Vuex 核心概念:四件套与单行道 本节摘要:Vuex 把共享状态收进单例仓库,用 state 存数据、getters 派生、mutations 同步变更、actions 处理异步,构成一条"组件 dispatch → action → commit → mutation → state → 视图"的单行道。本节实现一个完整的购物车 store,讲清同步异步红线与表单双绑这个高频坑。 学习目标 阅读完本节,你应当能够: 判断项目是否真的需要 Vuex,而不是跟风引入; 写出四件套职责清晰的 store 代码; 解释"异步只能放 action"的设计理由; 用Vuex 状态实现表单双向绑定的两种规范写法。 一、先回答要不要用 Vuex 解决的是多个组件共享同一状态的问题。

6.1 Vuex 核心概念:四件套与单行道

本节摘要:Vuex 把共享状态收进单例仓库,用 state 存数据、getters 派生、mutations 同步变更、actions 处理异步,构成一条"组件 dispatch → action → commit → mutation → state → 视图"的单行道。本节实现一个完整的购物车 store,讲清同步异步红线与表单双绑这个高频坑。

学习目标

阅读完本节,你应当能够:

  1. 判断项目是否真的需要 Vuex,而不是跟风引入;
  2. 写出四件套职责清晰的 store 代码;
  3. 解释"异步只能放 action"的设计理由;
  4. 用Vuex 状态实现表单双向绑定的两种规范写法。

一、先回答要不要用

Vuex 解决的是多个组件共享同一状态的问题。三个信号出现两个以上就该考虑:兄弟组件借共同父级中转数据、数据要跟着路由在多个页面间保留、某个状态被三个以上组件消费。反过来,父子传值清晰、页面之间无共享数据的项目,引入 Vuex 只会增加样板代码——它不是项目规模的勋章,是特定问题的药。

还有一个常被忽略的事实:Vuex 的 state 就是一个响应式对象,组件读它和读 data 没有本质区别,照样走 getter 登记依赖。仓库带来的不是新的响应机制,而是变更路径的收束——所有修改必须走 mutation,于是每次变更可追踪、可调试(开发者工具的时间旅行就是靠记录每次 mutation 实现的)。

二、四件套走一遍

一个够真实的购物车例子:

const store = new Vuex.Store({ // 数据源:唯一的真相源 state: { items: [], coupon: null }, // 派生值:仓库里的计算属性,自带缓存 getters: { total(state) { return state.items.reduce((s, i) => s + i.price * i.count, 0); }, discounted(state, getters) { return state.coupon ? getters.total * (1 - state.coupon.rate) : getters.total; } }, // 变更:同步、唯一合法的改 state 途径 mutations: { ADD_ITEM(state, item) { const found = state.items.find(i => i.id === item.id); found ? found.count++ : state.items.push({ ...item, count: 1 }); }, SET_COUPON(state, coupon) { state.coupon = coupon; } }, // 动作:可以异步,最后必须 commit 到 mutation actions: { async applyCoupon({ commit }, code) { const coupon = await fetchCoupon(code); // 异步请求 if (coupon) commit('SET_COUPON', coupon); } } });

数据流在仓库与组件之间的分工

数据流在仓库与组件之间的分工

组件里使用:

computed: { // 读 state 与 getters ...mapState(['items']), ...mapGetters(['discounted']) }, methods: { ...mapMutations(['ADD_ITEM']), ...mapActions(['applyCoupon']), on_click() { this.ADD_ITEM(this.goods); // 同步直接 commit this.applyCoupon('SALE2026'); // 异步走 action } }

注意 mutation 里 state.items.push 这个细节——第 2 章讲过 Vue 2 劫持了变更方法,所以在 mutation 里 push 是响应式安全的;这也是为什么新增对象属性必须用 set,仓库并不豁免这些规则。

三、同步异步红线为什么存在

初学者最常问:mutation 里为什么不能写 await?因为可追踪性依赖同步。开发者工具记录 mutation 时会立即快照 state;如果 mutation 是异步的,快照时机就说不清——记录到的是改之前还是改之后?时间旅行调试也就崩了。更深一层:同步 mutation 保证"一次 commit 对应一次确定的状态跃迁",并发场景下不会出现中间态被观察。

这条红线也定义了两者的分工语义:mutation 是"发生了什么"(事件语义,命名用动词过去式或全大写),action 是"怎么发生的"(流程语义,含异步与组合)。团队里把这条纪律守好,仓库代码的可读性远高于随处 await 直接改状态的野路子。

四、表单与仓库状态的双绑

v-model 直接绑 state 会绕过 mutation,踩到单向数据流红线(开发环境会有警告)。两种规范解法:

computed: { couponCode: { get() { return this.$store.state.couponCode; }, set(value) { this.$store.commit('SET_COUPON_CODE', value); // 写走 mutation } } }

输入不频繁的字段用这种 getter/setter 转接最干净;高频输入(每键一 commit,配合开发者工具会刷屏)可以本地暂存、失焦或提交时一次性 commit——这属于体验与可追踪性的取舍,团队定一条即可。

五、仓库之外的呼应:现代写法预览

同样的购物车,Pinia(Vue 3 时代官方推荐,6.2 详述)长这样:

export const useCartStore = defineStore('cart', { state: () => ({ items: [], coupon: null }), getters: { total: (s) => s.items.reduce((sum, i) => sum + i.price * i.count, 0) }, actions: { addItem(item) { const found = this.items.find(i => i.id === item.id); found ? found.count++ : this.items.push({ ...item, count: 1 }); } } });

mutation 层消失了,action 里直接改 state。为什么敢这么改、代价是什么,下一节展开。

本节要点回顾

再看一遍那张单行道图背后的分工逻辑,它其实回答了一个团队协作问题:谁有权改状态。没有仓库时,任何持有引用的组件都能改数据,出问题时排查范围是全组件树;有了仓库,所有修改收敛到 mutations 这个狭窄入口,排查范围骤缩为一组函数——这与数据库只允许通过事务改表的思路同源。很多团队引入 Vuex 后最直接的感受不是性能,而是"数据被改坏了"这类工单变得可定位了。另外提醒一点:仓库不是把所有状态都收上去,组件私有状态(展开折叠、输入草稿)留在组件里,仓库只收"共享"的那部分——什么都要过仓库的工程,会把简单问题复杂化,这也是初学者最容易矫枉过正的地方。

  • 判断:共享状态 + 多组件消费 + 跨页保留,三个信号出现两个才考虑仓库;
  • 单行道:dispatch → action(异步)→ commit → mutation(同步)→ state → 视图;
  • 红线理由:同步 mutation 保证状态跃迁确定可快照,时间旅行调试依赖它;
  • 表单:v-model 绑仓库状态必须经 getter/setter 或本地暂存转接;
  • 响应式底座不变:仓库的更新通知完全复用第 2 章的机制,只是状态主人换了。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U