前端知库

Vuex 的 state、getter、mutation、action 和 module 分别做什么?

Vuex 把共享状态放进一个 store。state 存数据,getter 是派生数据,mutation 是唯一修改 state 的地方且必须同步,action 处理异步再提交 mutation,module 把大 store 按业务拆开。新项目官方推荐 Pinia。

参考答案

简答

购物车数量要同时显示在顶栏、商品页和结算页,几个互不相干的组件都要读、都要改,靠 props 和事件一层层传很麻烦。Vuex 的做法是把这类共享状态集中放进一个 store,所有组件都从这里读,也都按同一套规则改。

store 有五个部分。state 存原始数据;getter 是从 state 算出来的派生数据,相当于 store 的 computed;mutation 是唯一能修改 state 的地方,组件用 commit 调用它;action 用来处理异步,比如先请求接口,拿到结果后再 commit 一个 mutation,组件用 dispatch 调用它;module 把一个大 store 按业务拆成多个小模块,开启 namespaced 后各自的名字互不冲突。

为什么要把改数据和异步分开?Vuex 文档要求 mutation 必须是同步函数。因为调试工具要在每次 mutation 前后记录状态快照,如果 mutation 里有异步回调,回调里改的数据就记录不到,状态变化没法追踪。所以异步都放在 action 里,action 本身不改 state,只在合适的时机提交 mutation。

要注意,Vue 官方的状态管理库已经换成了 Pinia。Vuex 3 和 4 仍然维护,但基本不会再加新功能,新项目推荐直接用 Pinia。

一个完整的 store

下面用一个购物车模块把五个部分串起来:

JavaScript
const cart = {
  namespaced: true,
  state: () => ({ items: [] }),
  getters: {
    count: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
  },
  mutations: {
    setItems(state, items) {
      state.items = items;
    },
  },
  actions: {
    async loadCart({ commit }) {
      const items = await fetch('/api/cart').then((res) => res.json());
      commit('setItems', items);
    },
  },
};

const store = new Vuex.Store({ modules: { cart } });

组件里读数量用 store.getters['cart/count'],加载购物车用 store.dispatch('cart/loadCart')。因为模块开了 namespaced: true,名字前面都要带上 cart/,别的模块也有 loadCart 也不会冲突。

loadCart 为什么不直接写 state.items = items?action 拿到的是一个上下文对象,按规则只通过 commit 修改数据。这样每一次数据变化都对应一条 mutation 记录,在调试工具里能看到是哪个 mutation、带了什么参数、改之前和改之后的 state。

模块的 state 写成函数,是为了同一个模块被注册多次时,每次都拿到一份新对象,不会共用同一份数据,和组件的 data 必须是函数是同一个道理。

刷新页面数据为什么没了

Vuex 的数据存在内存里,刷新页面后 store 会重新创建,state 回到初始值。需要保留的数据,比如登录信息、购物车,要自己存到 localStorage 或交给服务端,页面加载时再读回来。常见做法是写一个 Vuex 插件,订阅 mutation,每次变化后把需要的部分写进存储。

常见追问

Vuex 和 Pinia 有什么区别?

Pinia 去掉了 mutation,直接在 action 里改 state,同步异步都写在 action 里;没有嵌套模块,每个 store 是独立的,按需引入;对 TypeScript 的类型推导也更好。Vuex 的 README 里说可以把 Pinia 看作换了名字的 Vuex 5。Pinia 的用法见《Pinia 中 state、getter 和 action 应如何分工?》。

为什么不直接在组件里改 store.state?

能改,数据也会变,但绕过了 mutation,调试工具记录不到,出问题很难查是谁改的。开启 strict: true 后,在 mutation 之外修改 state 会抛错,开发环境可以用它检查,生产环境不要开,因为它要深度侦听整个状态树,开销很大。

mapState、mapGetters 这些辅助函数做了什么?

它们帮你生成组件的 computed 或 methods,比如 ...mapGetters('cart', ['count']) 等于在组件里写了一个返回 this.$store.getters['cart/count'] 的计算属性,省去重复代码。