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
下面用一个购物车模块把五个部分串起来:
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']的计算属性,省去重复代码。