1. 项目概述为什么一个 Vue 项目迟早要和 Vuex 打交道我带过不下二十个前端团队从三五人的小作坊到百人规模的中台部门几乎每个用 Vue.js 做中大型应用的团队都会在项目上线前两个月左右突然集体陷入一种“状态混乱焦虑症”——组件之间传参像打游击父子通信靠 props 和 $emit兄弟组件通信靠事件总线EventBus跨层级通信靠 provide/inject再复杂点就靠 localStorage 硬扛。结果就是某个按钮点击后三个页面同时刷新、两个弹窗莫名关闭、用户填写的表单数据在路由跳转后凭空消失……最后排查两小时发现是某个子组件悄悄改了父组件传来的对象引用而这个对象又被另一个组件 watch 着——整个状态链像一根绷紧又打结的橡皮筋一碰就崩。这就是 Vuex 存在的根本理由它不是为“炫技”而生而是为“止损”而来。Vuex 是 Vue 官方推荐的状态管理库核心价值在于强制约定状态变更路径、集中存储共享数据、提供可追溯的变更记录。它不解决“怎么写组件”而是解决“当组件越来越多、交互越来越复杂时谁在什么时候、以什么方式、改了哪部分数据”这个元问题。你不需要在 Vue 2 项目里一开始就上 Vuex——我见过太多团队在只有两个页面时就急着搭 store结果半年后发现 80% 的 mutation 永远不会被调用但你也绝不能等到所有页面都耦合成一团毛线球时才想起它——那时重构成本远超重写。标题里这个“Managing Vue.js State with Vuex”说白了就是给整个应用装上一套交通管制系统路口组件不再各自为政地指挥车流数据而是统一听从交通指挥中心store调度每辆车出发dispatch action、转弯commit mutation、到达state 更新都必须登记备案devtools 可追踪连红绿灯规则getter 计算逻辑都写进《城市交通法典》store/index.js而不是贴在每个路口的电线杆上。关键词里的getter、mutation正是这套系统的两个关键执法环节mutation 是唯一允许直接修改 state 的“红灯禁行区”必须同步执行getter 是只读的“路况显示屏”用于派生计算不触发更新。至于热搜词里反复出现的“vue3 中使用 Pinia 还是 Vuex”我的实操结论很直接新项目无脑选 Pinia但维护老项目、对接历史模块、或团队已深度绑定 Vuex 生态比如有大量现成的 Vuex 插件、中间件、devtools 扩展那 Vuex 依然是最稳的选择——它不是过时而是被更轻量的方案部分替代就像 jQuery 没死只是不再被默认推荐。2. Vuex 的核心设计哲学与五大模块拆解2.1 为什么是这五个属性它们不是并列关系而是有严格因果链Vuex 的 store 由state、getters、mutations、actions、modules五部分构成但新手常误以为这是五个“平级功能模块”。实际上它们构成了一条不可逆的数据流闭环state 是源头活水getters 是它的过滤器mutations 是它的唯一阀门actions 是触发阀门的遥控器modules 是把大水池划成小格子的隔板。理解这个链条比死记硬背“五个属性”重要十倍。state它不是普通对象而是 Vue 实例化时通过new Vue({ data: state })注入的响应式根对象。这意味着你对 state 的任何属性增删如state.user { name: 张三 }都会被 Vue 的响应式系统自动侦测但直接赋值新对象如state { user: {} }会彻底切断响应式——因为 Vue 无法劫持变量赋值操作。所以 Vuex 强制要求state 必须是函数返回的对象export default () ({ user: null })确保每次创建 store 实例都是全新响应式对象。getters它本质是 store 的 computed 属性。关键点在于getters 接收 state 作为第一个参数但可以接收其他 getters 作为第二个参数即getters对象本身。这使得你可以构建计算链比如userFullNamegetter 依赖userName和userLastName而userName又依赖userProfile。这种嵌套依赖会被 Vue 自动追踪只要底层 state 变所有相关 getter 都会重新求值。但注意getters 默认不缓存除非你在组件中通过mapGetters映射为计算属性或在 setup() 中用computed(() store.getters.xxx)包裹——否则每次访问store.getters.xxx都会重新执行函数。mutations这是 Vuex 最易被误解的部分。“mutation” 本意是“突变”但 Vuex 给它加了三重枷锁必须同步你不能在 mutation 里写setTimeout或axios.get()因为 devtools 需要精确捕获“前一刻 state 是什么这一刻 commit 后 state 变成什么”。异步操作会破坏这个时间戳链。必须命名每个 mutation 必须有字符串类型名如SET_USER_INFO而非匿名函数。这是为了 devtools 能显示可读日志“用户信息已更新” vs “[object Object]”。必须单一职责一个 mutation 只做一件事。比如SET_USER_INFO只负责合并用户数据不负责清除 tokenCLEAR_AUTH只负责清空认证字段不负责跳转登录页。这样单元测试才好写回滚才可控。actions它是 mutations 的“管家”。action 本身不改 state只负责调用异步 API如await api.getUser()根据 API 结果分发dispatch多个 mutation如commit(SET_USER_INFO, res.data)commit(SET_USER_PERMISSIONS, res.permissions)处理错误分支如catch中commit(SET_ERROR, err.message)关键细节action 函数接收context对象它包含{ commit, dispatch, state, getters, rootState, rootGetters }。但实际编码中我们几乎总是用 ES6 解构来简化export function fetchUser({ commit, getters }, userId) { ... }。这里getters是当前 module 的 getters若需跨 module必须显式写context.rootGetters[otherModule/someGetter]。modules当 state 膨胀到上千行你不可能把所有数据塞进一个index.js。modules 就是把 store 拆成多个子 store。每个 module 可有自己的 state、getters、mutations、actions。但要注意module 的 state 默认是局部的而 getters/mutations/actions 默认注册在全局命名空间。这意味着如果你有两个 module 都定义了setLoadingmutation不加命名空间就会冲突。解决方案是开启namespaced: true然后通过store.commit(user/setLoading, true)显式调用。我建议所有 module 必须开启 namespaced这是避免后期“命名地狱”的唯一防线。2.2 五大模块如何协同工作一个真实登录流程的全链路还原我们以最常见的“用户登录”场景为例走一遍 Vuex 全流程看五个模块如何咬合用户点击登录按钮→ 组件中调用this.$store.dispatch(user/login, { username, password })触发 actionsactions 接收参数发起 API 请求// modules/user.js const actions { async login({ commit, dispatch }, payload) { commit(SET_LOADING, true); // 同步更新 loading 状态 try { const res await api.login(payload); // 异步请求 commit(SET_USER_INFO, res.user); // 提交用户数据 commit(SET_TOKEN, res.token); dispatch(app/setLoginStatus, true, { root: true }); // 调用根 module } catch (err) { commit(SET_ERROR, err.message); throw err; // 便于组件捕获 } finally { commit(SET_LOADING, false); } } };actions 协调异步分发多个 mutationmutations 执行具体变更const mutations { SET_LOADING(state, isLoading) { state.loading isLoading; // 直接修改 state 属性 }, SET_USER_INFO(state, userInfo) { // 使用 Vue.set 确保新增属性响应式 Vue.set(state, userInfo, { ...userInfo }); }, SET_TOKEN(state, token) { state.token token; // 同时写入 localStorage保证刷新不丢失 localStorage.setItem(auth_token, token); } };mutations 是唯一修改 state 的入口且必须同步getters 提供派生状态const getters { isLoggedIn: state !!state.token, userName: state state.userInfo?.name || 游客, userPermissions: (state, getters) { // 依赖其他 getter实现权限组合 return getters.isLoggedIn ? state.userInfo?.permissions : []; } };getters 从 state 派生出业务语义更强的状态组件中消费template div v-ifisLoggedIn h2欢迎{{ userName }}/h2 button clicklogout v-ifhasPermission(admin)后台管理/button /div /template script import { mapGetters, mapActions } from vuex; export default { computed: { ...mapGetters(user, [isLoggedIn, userName]), ...mapGetters([hasPermission]) // 从根 getters 获取 }, methods: { ...mapActions(user, [logout]) } } /script组件通过 mapXXX 工具函数将 store 能力映射为本地属性/方法这个流程清晰展示了state 是数据容器getters 是数据加工厂mutations 是数据手术刀actions 是手术室主任modules 是划分科室的医院架构。任何环节缺失或错位都会导致状态失控。3. 从零搭建 Vuex Store手把手配置与避坑指南3.1 初始化 StoreVue 2 与 Vue 3 的关键差异虽然标题是 “Managing Vue.js State with Vuex”但必须明确Vuex 4 是专为 Vue 3 设计的版本Vuex 3 仅支持 Vue 2。很多团队踩坑源于混淆版本。以下是两个版本初始化的核心代码对比与原理说明Vue 2 Vuex 3经典写法npm install vuex3// store/index.js import Vue from vue; import Vuex from vuex; Vue.use(Vuex); // 必须调用注册 Vuex 插件 export default new Vuex.Store({ state: { count: 0 }, mutations: { INCREMENT(state) { state.count; } } });// main.js import store from ./store; new Vue({ store, // 直接注入 render: h h(App) }).$mount(#app);提示Vue.use(Vuex)是关键一步。它会向 Vue 构造函数注入$store实例并在所有组件中可用。如果忘记这行this.$store将是 undefined。Vue 3 Vuex 4Composition API 友好npm install vuex4// store/index.js import { createStore } from vuex; export default createStore({ state() { return { count: 0 }; }, mutations: { INCREMENT(state) { state.count; } } });// main.js import { createApp } from vue; import { store } from ./store; // 注意Vuex 4 的 store 是普通对象非 Vue 插件 import App from ./App.vue; const app createApp(App); app.use(store); // 调用 use 方法安装 app.mount(#app);注意Vuex 4 不再需要Vue.use()而是通过app.use(store)注册。这是因为 Vue 3 的插件机制改为函数式createStore返回的是一个符合插件协议的对象。为什么必须区分因为 Vuex 4 移除了对 Vue 2 的兼容层其内部完全基于 Vue 3 的响应式 APIreactive,ref重构。如果你在 Vue 3 项目中错误安装 Vuex 3控制台会报错Cannot read property use of undefined反之在 Vue 2 项目装 Vuex 4则store无法被 Vue 实例识别。3.2 Modules 模块化实战如何科学划分 Store 结构一个中型电商项目store 目录结构应该长什么样我给出经过 7 个项目验证的黄金模板store/ ├── index.js # 根 store 配置合并所有 modules ├── modules/ │ ├── user.js # 用户认证、个人信息 │ ├── cart.js # 购物车状态、商品列表 │ ├── product.js # 商品详情、分类树、搜索历史 │ └── order.js # 订单列表、订单详情、支付状态 ├── plugins/ # 自定义插件目录如持久化、日志 │ └── persist.js # 将 state 持久化到 localStorage └── utils/ # 工具函数如 deepClone, mergeState每个 module 的标准写法以 user.js 为例// store/modules/user.js const state () ({ token: localStorage.getItem(auth_token) || , userInfo: null, loading: false, error: null }); const getters { isLoggedIn: state !!state.token, userName: state state.userInfo?.name || 游客, // 注意getter 名称应体现业务语义而非技术动作 hasTokenExpired: state { if (!state.token) return true; const payload JSON.parse(atob(state.token.split(.)[1])); return Date.now() payload.exp * 1000; } }; const mutations { // 命名规范动词名词全部大写下划线分隔 SET_TOKEN(state, token) { state.token token; if (token) { localStorage.setItem(auth_token, token); } else { localStorage.removeItem(auth_token); } }, SET_USER_INFO(state, userInfo) { // 使用 Vue.set 或 Object.assign 确保响应式 state.userInfo { ...userInfo }; }, SET_LOADING(state, isLoading) { state.loading isLoading; }, SET_ERROR(state, error) { state.error error; } }; const actions { // action 名称用驼峰体现业务意图 async login({ commit, dispatch }, credentials) { commit(SET_LOADING, true); try { const res await api.login(credentials); commit(SET_TOKEN, res.token); commit(SET_USER_INFO, res.user); // 登录成功后自动获取用户权限 await dispatch(fetchPermissions); } catch (err) { commit(SET_ERROR, err.response?.data?.message || 登录失败); throw err; } finally { commit(SET_LOADING, false); } }, async fetchPermissions({ commit, state }) { if (!state.token) return; try { const res await api.getPermissions(); // 权限数据通常需要合并到 userInfo 中 commit(SET_USER_INFO, { ...state.userInfo, permissions: res }); } catch (err) { console.error(获取权限失败, err); } } }; // 必须导出 namespaced: true export default { namespaced: true, state, getters, mutations, actions };关键避坑点state 必须是函数防止多个 store 实例共享同一对象引用。localStorage 同步时机SET_TOKEN中同时操作state.token和localStorage确保内存与磁盘一致CLEAR_TOKEN时必须removeItem否则下次启动仍会读取旧 token。mutation 命名一致性全大写下划线是社区约定比setToken更易在 devtools 中识别。action 错误处理try/catch中throw err是为了让调用方组件能await dispatch().catch()实现错误冒泡。3.3 DevTools 集成不只是看状态更是调试利器Vuex DevTools 是 Chrome 和 Edge 浏览器的官方扩展但它绝不仅是“查看 state”的工具。我把它当作 Vue 应用的“黑匣子”日常调试三大核心用法1. 时间旅行Time Travel点击 devtools 中的任意一次 mutation 记录state 会瞬间回滚到该时刻。这让你能精准复现“用户点了什么按钮后页面变空白”这类问题。操作路径DevTools → “Mutation” 标签 → 点击某条记录左侧的 ▶️ 按钮。2. 提交过滤与搜索大型项目 mutation 数量庞大可直接在顶部搜索框输入USER_只显示用户模块相关变更或点击右上角齿轮图标勾选 “Filter commits by module”按 module 分组查看。3. 手动触发 Mutation高级技巧在 “Mutation” 标签页点击右上角 “” 按钮可手动输入 mutation 名称和 payload模拟任意状态变更。例如输入USER/SET_ERRORpayload 填{message: 网络超时}立即看到错误提示是否正确渲染——这比写测试用例快十倍。安装与启用步骤Edge 浏览器打开 Microsoft Edge Add-ons 商店搜索 “Vue.js devtools”点击“获取”安装。在 Vue 项目中确保 store 创建时传入devtools: trueVue 2 默认开启Vue 3 需显式设置// Vue 3 Vuex 4 export default createStore({ // ...其他配置 devtools: process.env.NODE_ENV development // 仅开发环境开启 });启动项目在浏览器按 F12切换到 “Vue” 标签页即可看到 Vuex 面板。注意如果看不到 Vuex 标签请检查① 是否安装了最新版 Vue Devtools非旧版 Vue.js devtools② 项目是否在本地http://localhost运行生产环境域名默认禁用③ store 是否正确app.use(store)。4. 核心实操环节从需求到代码的完整落地4.1 场景还原购物车状态管理——为什么不能只用组件 data假设你正在开发一个电商首页顶部导航栏需显示购物车商品数量如 “购物车 (3)”而商品列表页的每个商品卡片都有“加入购物车”按钮。表面看这似乎可以用一个全局 event bus 解决商品卡片点击 →bus.$emit(add-to-cart, item)导航栏监听 →bus.$on(add-to-cart, () this.cartCount)但很快你会遇到三个致命问题状态不一致用户在商品页点击“加入”导航栏数字1但用户刷新页面数字归零——因为 event bus 数据不持久。来源不可溯当 cartCount 突然变成 999你无法知道是哪个组件、在什么条件下触发的。逻辑碎片化计算“总价”、“是否满减”、“商品去重”等逻辑散落在各个组件的 methods 里修改一处八处报错。Vuex 的解法是把购物车抽象为一个独立的业务实体其状态、行为、规则全部收口到 cart module。cart module 完整代码// store/modules/cart.js const state () ({ items: [], // 商品数组每个 item 包含 id, name, price, quantity loading: false, error: null }); const getters { // 购物车总数量所有商品数量之和 totalCount: state state.items.reduce((sum, item) sum item.quantity, 0), // 购物车总金额单价 * 数量 求和 totalPrice: state state.items.reduce((sum, item) sum item.price * item.quantity, 0), // 是否为空 isEmpty: state state.items.length 0, // 获取指定商品用于判断是否已存在 itemById: state id state.items.find(item item.id id), // 满减活动满 200 减 20 discountAmount: (state, getters) getters.totalPrice 200 ? 20 : 0, finalPrice: (state, getters) getters.totalPrice - getters.discountAmount }; const mutations { // 添加商品已存在则 quantity1否则 push 新 item ADD_ITEM(state, newItem) { const existing state.items.find(item item.id newItem.id); if (existing) { existing.quantity 1; } else { state.items.push({ ...newItem, quantity: 1 }); } }, // 更新商品数量 UPDATE_QUANTITY(state, { id, quantity }) { const item state.items.find(item item.id id); if (item) { item.quantity Math.max(1, quantity); // 至少为 1 } }, // 删除商品 REMOVE_ITEM(state, id) { state.items state.items.filter(item item.id ! id); }, // 清空购物车 CLEAR_CART(state) { state.items []; }, SET_LOADING(state, isLoading) { state.loading isLoading; }, SET_ERROR(state, error) { state.error error; } }; const actions { // 异步添加先校验库存再更新 async addItem({ commit, getters }, item) { commit(SET_LOADING, true); try { // 模拟 API 校验库存 const stock await api.checkStock(item.id); if (stock 1) { throw new Error(${item.name} 库存不足); } // 库存充足执行本地添加 commit(ADD_ITEM, item); // 触发全局通知可选 this._vm.$message.success(${item.name} 已加入购物车); } catch (err) { commit(SET_ERROR, err.message); throw err; } finally { commit(SET_LOADING, false); } }, // 批量添加如从收藏夹一键加入 addItems({ commit }, items) { items.forEach(item commit(ADD_ITEM, item)); }, // 同步购物车从服务器拉取最新状态如用户在其他设备操作后 async syncCart({ commit, state }) { try { const serverCart await api.getCart(); // 服务端返回的是完整 cart 数组直接替换本地 state commit(REPLACE_CART, serverCart); } catch (err) { console.error(同步购物车失败, err); } } }; // 注意这里我们额外定义了一个 mutation 用于替换整个 cart // 因为服务端返回的 cart 可能包含本地没有的字段如优惠券信息 const REPLACE_CART (state, serverItems) { state.items serverItems.map(item ({ id: item.id, name: item.name, price: item.price, quantity: item.quantity, // 保留服务端特有字段 coupon: item.coupon })); }; export default { namespaced: true, state, getters, mutations: { ...mutations, REPLACE_CART }, actions };组件中调用示例商品卡片template div classproduct-card h3{{ product.name }}/h3 p¥{{ product.price }}/p button clickaddToCart :disabledisAdding {{ isAdding ? 添加中... : 加入购物车 }} /button /div /template script import { mapActions, mapState, mapGetters } from vuex; export default { props: { product: { type: Object, required: true } }, data() { return { isAdding: false }; }, methods: { // 映射 cart module 的 action ...mapActions(cart, [addItem]), async addToCart() { this.isAdding true; try { await this.addItem(this.product); } catch (err) { this.$message.error(err.message); } finally { this.isAdding false; } } } }; /script导航栏中显示数量全局组件template nav classheader router-link to/cart购物车 ({{ cartCount }})/router-link /nav /template script import { mapState, mapGetters } from vuex; export default { computed: { // 直接使用 cart module 的 getter ...mapGetters(cart, [totalCount]), cartCount() { return this.totalCount; } } }; /script这个例子证明Vuex 不是增加复杂度而是把隐式的、分散的、易出错的状态流转变成显式的、集中的、可测试的业务契约。当你需要新增“购物车分享”功能时只需在 cart module 中添加shareCartaction 和对应 mutation所有组件自动获得能力无需修改任何视图代码。4.2 持久化插件让购物车在刷新后依然存在Vuex 默认是内存状态页面刷新即丢失。但购物车、用户偏好等数据必须持久化。官方不提供内置方案但社区有成熟插件vuex-persistedstate。不过我更推荐手写一个轻量级持久化插件原因有三vuex-persistedstate会序列化整个 state若 state 包含函数、Date 对象、RegExp会导致JSON.stringify报错它默认持久化所有 state但你可能只想存 cart不想存 loading 状态手写插件能精准控制序列化/反序列化逻辑比如对 token 做加密存储。自定义持久化插件代码store/plugins/persist.js// store/plugins/persist.js export default function createPersistPlugin(options {}) { const { key vuex-store, // localStorage key paths [], // 需要持久化的 state 路径如 [cart.items, user.token] storage window.localStorage // 可替换为 sessionStorage 或自定义存储 } options; // 从 storage 加载初始 state let savedState {}; try { const json storage.getItem(key); if (json) { savedState JSON.parse(json); } } catch (e) { console.warn(Failed to load persisted state, e); } // 创建插件函数 return store { // 初始化将 savedState 合并到当前 store.state if (Object.keys(savedState).length 0) { // 递归合并只覆盖 paths 指定的路径 paths.forEach(path { const keys path.split(.); let target store.state; let source savedState; for (let i 0; i keys.length - 1; i) { target target[keys[i]]; source source[keys[i]]; } if (target source keys[keys.length - 1] in source) { target[keys[keys.length - 1]] source[keys[keys.length - 1]]; } }); } // 订阅 mutation当指定路径的 state 变更时保存到 storage store.subscribe((mutation, state) { // 只监听 paths 中的路径变更 for (const path of paths) { const keys path.split(.); let current state; for (const key of keys) { if (current typeof current object) { current current[key]; } else { break; } } if (current ! undefined) { // 该路径有值需要保存 try { const json JSON.stringify(savedState); storage.setItem(key, json); } catch (e) { console.warn(Failed to save persisted state, e); } break; // 找到一个就保存避免重复 } } }); }; }在 store/index.js 中使用import createPersistPlugin from ./plugins/persist; const store createStore({ // ...其他配置 plugins: [ createPersistPlugin({ key: my-shop-store, paths: [cart.items, user.token, app.theme] // 精确控制哪些数据持久化 }) ] });提示这个插件的关键优势在于paths参数。它允许你声明式地指定“只存 cart.items 数组不存 cart.loading 状态”避免把临时状态也写入 localStorage导致下次启动时 loading 一直为 true。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表从报错信息直击根源报错信息根本原因排查步骤解决方案TypeError: Cannot read property xxx of undefined在组件中访问了未定义的 getter 或 state 属性1. 检查mapGetters映射的名称是否拼写正确2. 检查该 getter 是否在对应 module 中定义3. 检查 module 是否开启了namespaced: true且映射时加了前缀使用mapGetters(moduleName, [getterName])或在 getter 中添加防御性判断state?.userInfo?.nameError: [vuex] do not mutate vuex store state outside mutation handlers.在 action、getter 或组件中直接修改了 state如state.count1. 全局搜索state.确认所有修改都发生在 mutation 内2. 检查是否误用了Object.assign(state, newData)应改为Object.assign(state, newData)仅适用于浅拷贝深层对象需用Vue.set严格遵守所有 state 修改必须在 mutation 中且只能通过state.xxx value或Vue.set(state, xxx, value)Unknown mutation type: xxxcommit 的 mutation 名称与 store 中定义的不一致1. 检查 mutation 名称是否全大写、下划线分隔2. 检查是否在 namespaced module 中却忘了加前缀如commit(cart/ADD_ITEM)3. 检查是否在 action 中误用了dispatch而非commit使用常量定义 mutation 名export const ADD_ITEM cart/ADD_ITEM在 commit 时commit(ADD_ITEM, payload)避免字符串硬编码store is not definedstore 未正确注入 Vue 实例1. Vue 2检查main.js中是否漏掉Vue.use(Vuex)2. Vue 3检查main.js中是否漏掉app.use(store)3. 检查 store 文件是否正确导出Vue 2 是export default new Vuex.Store({...})Vue 3 是export default createStore({...})在main.js中打印console.log(store)确认对象存在检查浏览器控制台是否有语法错误阻断执行getters are not reactive在 setup() 中直接访问store.getters.xxx未用computed包裹1. 检查是否写了const userName store.getters.userName错误2. 正确写法应为const userName computed(() store.getters.userName)所有对 getters 的访问必须包裹在computed中否则失去响应式5.2 我踩过的三个深坑与独家解决方案坑一在 mutation 中使用this导致 this 指向丢失现象mutation 函数里写了this.$http.get(...)运行时报错Cannot read property get of undefined。原因Vuex 的 mutation 是纯函数不绑定任何上下文this指向undefined严格模式下。解决方案绝对不要在 mutation 中访问this。API 调用必须放在 action 中mutation 只负责同步更新 state。如果确实需要在 mutation 中访问全局对象如 router应在 store 创建时通过插件注入// store/index.js import router from /router; export default createStore({ // ... plugins: [ store { // 将 router 注入 store 实例 store.router router; } ] }); // mutation 中 SET_ROUTE(state, route) { store.router.push(route); // 此时 store.router 可用 }坑二getters 中的异步操作导致无限循环现象在 getter 中写了async function getUser() { return await api.getUser(); }页面卡死。原因getter 必须是同步函数返回值会被 Vue 的响应式系统追踪。async函数返回 PromisePromise 对象没有响应式属性导致 Vue 无法建立依赖关系进而反复求值。解决方案getters 中禁止任何异步操作。需要异步数据必须在 action 中获取
Vuex状态管理核心原理与实战:从混乱到可控
发布时间:2026/6/22 3:41:04
1. 项目概述为什么一个 Vue 项目迟早要和 Vuex 打交道我带过不下二十个前端团队从三五人的小作坊到百人规模的中台部门几乎每个用 Vue.js 做中大型应用的团队都会在项目上线前两个月左右突然集体陷入一种“状态混乱焦虑症”——组件之间传参像打游击父子通信靠 props 和 $emit兄弟组件通信靠事件总线EventBus跨层级通信靠 provide/inject再复杂点就靠 localStorage 硬扛。结果就是某个按钮点击后三个页面同时刷新、两个弹窗莫名关闭、用户填写的表单数据在路由跳转后凭空消失……最后排查两小时发现是某个子组件悄悄改了父组件传来的对象引用而这个对象又被另一个组件 watch 着——整个状态链像一根绷紧又打结的橡皮筋一碰就崩。这就是 Vuex 存在的根本理由它不是为“炫技”而生而是为“止损”而来。Vuex 是 Vue 官方推荐的状态管理库核心价值在于强制约定状态变更路径、集中存储共享数据、提供可追溯的变更记录。它不解决“怎么写组件”而是解决“当组件越来越多、交互越来越复杂时谁在什么时候、以什么方式、改了哪部分数据”这个元问题。你不需要在 Vue 2 项目里一开始就上 Vuex——我见过太多团队在只有两个页面时就急着搭 store结果半年后发现 80% 的 mutation 永远不会被调用但你也绝不能等到所有页面都耦合成一团毛线球时才想起它——那时重构成本远超重写。标题里这个“Managing Vue.js State with Vuex”说白了就是给整个应用装上一套交通管制系统路口组件不再各自为政地指挥车流数据而是统一听从交通指挥中心store调度每辆车出发dispatch action、转弯commit mutation、到达state 更新都必须登记备案devtools 可追踪连红绿灯规则getter 计算逻辑都写进《城市交通法典》store/index.js而不是贴在每个路口的电线杆上。关键词里的getter、mutation正是这套系统的两个关键执法环节mutation 是唯一允许直接修改 state 的“红灯禁行区”必须同步执行getter 是只读的“路况显示屏”用于派生计算不触发更新。至于热搜词里反复出现的“vue3 中使用 Pinia 还是 Vuex”我的实操结论很直接新项目无脑选 Pinia但维护老项目、对接历史模块、或团队已深度绑定 Vuex 生态比如有大量现成的 Vuex 插件、中间件、devtools 扩展那 Vuex 依然是最稳的选择——它不是过时而是被更轻量的方案部分替代就像 jQuery 没死只是不再被默认推荐。2. Vuex 的核心设计哲学与五大模块拆解2.1 为什么是这五个属性它们不是并列关系而是有严格因果链Vuex 的 store 由state、getters、mutations、actions、modules五部分构成但新手常误以为这是五个“平级功能模块”。实际上它们构成了一条不可逆的数据流闭环state 是源头活水getters 是它的过滤器mutations 是它的唯一阀门actions 是触发阀门的遥控器modules 是把大水池划成小格子的隔板。理解这个链条比死记硬背“五个属性”重要十倍。state它不是普通对象而是 Vue 实例化时通过new Vue({ data: state })注入的响应式根对象。这意味着你对 state 的任何属性增删如state.user { name: 张三 }都会被 Vue 的响应式系统自动侦测但直接赋值新对象如state { user: {} }会彻底切断响应式——因为 Vue 无法劫持变量赋值操作。所以 Vuex 强制要求state 必须是函数返回的对象export default () ({ user: null })确保每次创建 store 实例都是全新响应式对象。getters它本质是 store 的 computed 属性。关键点在于getters 接收 state 作为第一个参数但可以接收其他 getters 作为第二个参数即getters对象本身。这使得你可以构建计算链比如userFullNamegetter 依赖userName和userLastName而userName又依赖userProfile。这种嵌套依赖会被 Vue 自动追踪只要底层 state 变所有相关 getter 都会重新求值。但注意getters 默认不缓存除非你在组件中通过mapGetters映射为计算属性或在 setup() 中用computed(() store.getters.xxx)包裹——否则每次访问store.getters.xxx都会重新执行函数。mutations这是 Vuex 最易被误解的部分。“mutation” 本意是“突变”但 Vuex 给它加了三重枷锁必须同步你不能在 mutation 里写setTimeout或axios.get()因为 devtools 需要精确捕获“前一刻 state 是什么这一刻 commit 后 state 变成什么”。异步操作会破坏这个时间戳链。必须命名每个 mutation 必须有字符串类型名如SET_USER_INFO而非匿名函数。这是为了 devtools 能显示可读日志“用户信息已更新” vs “[object Object]”。必须单一职责一个 mutation 只做一件事。比如SET_USER_INFO只负责合并用户数据不负责清除 tokenCLEAR_AUTH只负责清空认证字段不负责跳转登录页。这样单元测试才好写回滚才可控。actions它是 mutations 的“管家”。action 本身不改 state只负责调用异步 API如await api.getUser()根据 API 结果分发dispatch多个 mutation如commit(SET_USER_INFO, res.data)commit(SET_USER_PERMISSIONS, res.permissions)处理错误分支如catch中commit(SET_ERROR, err.message)关键细节action 函数接收context对象它包含{ commit, dispatch, state, getters, rootState, rootGetters }。但实际编码中我们几乎总是用 ES6 解构来简化export function fetchUser({ commit, getters }, userId) { ... }。这里getters是当前 module 的 getters若需跨 module必须显式写context.rootGetters[otherModule/someGetter]。modules当 state 膨胀到上千行你不可能把所有数据塞进一个index.js。modules 就是把 store 拆成多个子 store。每个 module 可有自己的 state、getters、mutations、actions。但要注意module 的 state 默认是局部的而 getters/mutations/actions 默认注册在全局命名空间。这意味着如果你有两个 module 都定义了setLoadingmutation不加命名空间就会冲突。解决方案是开启namespaced: true然后通过store.commit(user/setLoading, true)显式调用。我建议所有 module 必须开启 namespaced这是避免后期“命名地狱”的唯一防线。2.2 五大模块如何协同工作一个真实登录流程的全链路还原我们以最常见的“用户登录”场景为例走一遍 Vuex 全流程看五个模块如何咬合用户点击登录按钮→ 组件中调用this.$store.dispatch(user/login, { username, password })触发 actionsactions 接收参数发起 API 请求// modules/user.js const actions { async login({ commit, dispatch }, payload) { commit(SET_LOADING, true); // 同步更新 loading 状态 try { const res await api.login(payload); // 异步请求 commit(SET_USER_INFO, res.user); // 提交用户数据 commit(SET_TOKEN, res.token); dispatch(app/setLoginStatus, true, { root: true }); // 调用根 module } catch (err) { commit(SET_ERROR, err.message); throw err; // 便于组件捕获 } finally { commit(SET_LOADING, false); } } };actions 协调异步分发多个 mutationmutations 执行具体变更const mutations { SET_LOADING(state, isLoading) { state.loading isLoading; // 直接修改 state 属性 }, SET_USER_INFO(state, userInfo) { // 使用 Vue.set 确保新增属性响应式 Vue.set(state, userInfo, { ...userInfo }); }, SET_TOKEN(state, token) { state.token token; // 同时写入 localStorage保证刷新不丢失 localStorage.setItem(auth_token, token); } };mutations 是唯一修改 state 的入口且必须同步getters 提供派生状态const getters { isLoggedIn: state !!state.token, userName: state state.userInfo?.name || 游客, userPermissions: (state, getters) { // 依赖其他 getter实现权限组合 return getters.isLoggedIn ? state.userInfo?.permissions : []; } };getters 从 state 派生出业务语义更强的状态组件中消费template div v-ifisLoggedIn h2欢迎{{ userName }}/h2 button clicklogout v-ifhasPermission(admin)后台管理/button /div /template script import { mapGetters, mapActions } from vuex; export default { computed: { ...mapGetters(user, [isLoggedIn, userName]), ...mapGetters([hasPermission]) // 从根 getters 获取 }, methods: { ...mapActions(user, [logout]) } } /script组件通过 mapXXX 工具函数将 store 能力映射为本地属性/方法这个流程清晰展示了state 是数据容器getters 是数据加工厂mutations 是数据手术刀actions 是手术室主任modules 是划分科室的医院架构。任何环节缺失或错位都会导致状态失控。3. 从零搭建 Vuex Store手把手配置与避坑指南3.1 初始化 StoreVue 2 与 Vue 3 的关键差异虽然标题是 “Managing Vue.js State with Vuex”但必须明确Vuex 4 是专为 Vue 3 设计的版本Vuex 3 仅支持 Vue 2。很多团队踩坑源于混淆版本。以下是两个版本初始化的核心代码对比与原理说明Vue 2 Vuex 3经典写法npm install vuex3// store/index.js import Vue from vue; import Vuex from vuex; Vue.use(Vuex); // 必须调用注册 Vuex 插件 export default new Vuex.Store({ state: { count: 0 }, mutations: { INCREMENT(state) { state.count; } } });// main.js import store from ./store; new Vue({ store, // 直接注入 render: h h(App) }).$mount(#app);提示Vue.use(Vuex)是关键一步。它会向 Vue 构造函数注入$store实例并在所有组件中可用。如果忘记这行this.$store将是 undefined。Vue 3 Vuex 4Composition API 友好npm install vuex4// store/index.js import { createStore } from vuex; export default createStore({ state() { return { count: 0 }; }, mutations: { INCREMENT(state) { state.count; } } });// main.js import { createApp } from vue; import { store } from ./store; // 注意Vuex 4 的 store 是普通对象非 Vue 插件 import App from ./App.vue; const app createApp(App); app.use(store); // 调用 use 方法安装 app.mount(#app);注意Vuex 4 不再需要Vue.use()而是通过app.use(store)注册。这是因为 Vue 3 的插件机制改为函数式createStore返回的是一个符合插件协议的对象。为什么必须区分因为 Vuex 4 移除了对 Vue 2 的兼容层其内部完全基于 Vue 3 的响应式 APIreactive,ref重构。如果你在 Vue 3 项目中错误安装 Vuex 3控制台会报错Cannot read property use of undefined反之在 Vue 2 项目装 Vuex 4则store无法被 Vue 实例识别。3.2 Modules 模块化实战如何科学划分 Store 结构一个中型电商项目store 目录结构应该长什么样我给出经过 7 个项目验证的黄金模板store/ ├── index.js # 根 store 配置合并所有 modules ├── modules/ │ ├── user.js # 用户认证、个人信息 │ ├── cart.js # 购物车状态、商品列表 │ ├── product.js # 商品详情、分类树、搜索历史 │ └── order.js # 订单列表、订单详情、支付状态 ├── plugins/ # 自定义插件目录如持久化、日志 │ └── persist.js # 将 state 持久化到 localStorage └── utils/ # 工具函数如 deepClone, mergeState每个 module 的标准写法以 user.js 为例// store/modules/user.js const state () ({ token: localStorage.getItem(auth_token) || , userInfo: null, loading: false, error: null }); const getters { isLoggedIn: state !!state.token, userName: state state.userInfo?.name || 游客, // 注意getter 名称应体现业务语义而非技术动作 hasTokenExpired: state { if (!state.token) return true; const payload JSON.parse(atob(state.token.split(.)[1])); return Date.now() payload.exp * 1000; } }; const mutations { // 命名规范动词名词全部大写下划线分隔 SET_TOKEN(state, token) { state.token token; if (token) { localStorage.setItem(auth_token, token); } else { localStorage.removeItem(auth_token); } }, SET_USER_INFO(state, userInfo) { // 使用 Vue.set 或 Object.assign 确保响应式 state.userInfo { ...userInfo }; }, SET_LOADING(state, isLoading) { state.loading isLoading; }, SET_ERROR(state, error) { state.error error; } }; const actions { // action 名称用驼峰体现业务意图 async login({ commit, dispatch }, credentials) { commit(SET_LOADING, true); try { const res await api.login(credentials); commit(SET_TOKEN, res.token); commit(SET_USER_INFO, res.user); // 登录成功后自动获取用户权限 await dispatch(fetchPermissions); } catch (err) { commit(SET_ERROR, err.response?.data?.message || 登录失败); throw err; } finally { commit(SET_LOADING, false); } }, async fetchPermissions({ commit, state }) { if (!state.token) return; try { const res await api.getPermissions(); // 权限数据通常需要合并到 userInfo 中 commit(SET_USER_INFO, { ...state.userInfo, permissions: res }); } catch (err) { console.error(获取权限失败, err); } } }; // 必须导出 namespaced: true export default { namespaced: true, state, getters, mutations, actions };关键避坑点state 必须是函数防止多个 store 实例共享同一对象引用。localStorage 同步时机SET_TOKEN中同时操作state.token和localStorage确保内存与磁盘一致CLEAR_TOKEN时必须removeItem否则下次启动仍会读取旧 token。mutation 命名一致性全大写下划线是社区约定比setToken更易在 devtools 中识别。action 错误处理try/catch中throw err是为了让调用方组件能await dispatch().catch()实现错误冒泡。3.3 DevTools 集成不只是看状态更是调试利器Vuex DevTools 是 Chrome 和 Edge 浏览器的官方扩展但它绝不仅是“查看 state”的工具。我把它当作 Vue 应用的“黑匣子”日常调试三大核心用法1. 时间旅行Time Travel点击 devtools 中的任意一次 mutation 记录state 会瞬间回滚到该时刻。这让你能精准复现“用户点了什么按钮后页面变空白”这类问题。操作路径DevTools → “Mutation” 标签 → 点击某条记录左侧的 ▶️ 按钮。2. 提交过滤与搜索大型项目 mutation 数量庞大可直接在顶部搜索框输入USER_只显示用户模块相关变更或点击右上角齿轮图标勾选 “Filter commits by module”按 module 分组查看。3. 手动触发 Mutation高级技巧在 “Mutation” 标签页点击右上角 “” 按钮可手动输入 mutation 名称和 payload模拟任意状态变更。例如输入USER/SET_ERRORpayload 填{message: 网络超时}立即看到错误提示是否正确渲染——这比写测试用例快十倍。安装与启用步骤Edge 浏览器打开 Microsoft Edge Add-ons 商店搜索 “Vue.js devtools”点击“获取”安装。在 Vue 项目中确保 store 创建时传入devtools: trueVue 2 默认开启Vue 3 需显式设置// Vue 3 Vuex 4 export default createStore({ // ...其他配置 devtools: process.env.NODE_ENV development // 仅开发环境开启 });启动项目在浏览器按 F12切换到 “Vue” 标签页即可看到 Vuex 面板。注意如果看不到 Vuex 标签请检查① 是否安装了最新版 Vue Devtools非旧版 Vue.js devtools② 项目是否在本地http://localhost运行生产环境域名默认禁用③ store 是否正确app.use(store)。4. 核心实操环节从需求到代码的完整落地4.1 场景还原购物车状态管理——为什么不能只用组件 data假设你正在开发一个电商首页顶部导航栏需显示购物车商品数量如 “购物车 (3)”而商品列表页的每个商品卡片都有“加入购物车”按钮。表面看这似乎可以用一个全局 event bus 解决商品卡片点击 →bus.$emit(add-to-cart, item)导航栏监听 →bus.$on(add-to-cart, () this.cartCount)但很快你会遇到三个致命问题状态不一致用户在商品页点击“加入”导航栏数字1但用户刷新页面数字归零——因为 event bus 数据不持久。来源不可溯当 cartCount 突然变成 999你无法知道是哪个组件、在什么条件下触发的。逻辑碎片化计算“总价”、“是否满减”、“商品去重”等逻辑散落在各个组件的 methods 里修改一处八处报错。Vuex 的解法是把购物车抽象为一个独立的业务实体其状态、行为、规则全部收口到 cart module。cart module 完整代码// store/modules/cart.js const state () ({ items: [], // 商品数组每个 item 包含 id, name, price, quantity loading: false, error: null }); const getters { // 购物车总数量所有商品数量之和 totalCount: state state.items.reduce((sum, item) sum item.quantity, 0), // 购物车总金额单价 * 数量 求和 totalPrice: state state.items.reduce((sum, item) sum item.price * item.quantity, 0), // 是否为空 isEmpty: state state.items.length 0, // 获取指定商品用于判断是否已存在 itemById: state id state.items.find(item item.id id), // 满减活动满 200 减 20 discountAmount: (state, getters) getters.totalPrice 200 ? 20 : 0, finalPrice: (state, getters) getters.totalPrice - getters.discountAmount }; const mutations { // 添加商品已存在则 quantity1否则 push 新 item ADD_ITEM(state, newItem) { const existing state.items.find(item item.id newItem.id); if (existing) { existing.quantity 1; } else { state.items.push({ ...newItem, quantity: 1 }); } }, // 更新商品数量 UPDATE_QUANTITY(state, { id, quantity }) { const item state.items.find(item item.id id); if (item) { item.quantity Math.max(1, quantity); // 至少为 1 } }, // 删除商品 REMOVE_ITEM(state, id) { state.items state.items.filter(item item.id ! id); }, // 清空购物车 CLEAR_CART(state) { state.items []; }, SET_LOADING(state, isLoading) { state.loading isLoading; }, SET_ERROR(state, error) { state.error error; } }; const actions { // 异步添加先校验库存再更新 async addItem({ commit, getters }, item) { commit(SET_LOADING, true); try { // 模拟 API 校验库存 const stock await api.checkStock(item.id); if (stock 1) { throw new Error(${item.name} 库存不足); } // 库存充足执行本地添加 commit(ADD_ITEM, item); // 触发全局通知可选 this._vm.$message.success(${item.name} 已加入购物车); } catch (err) { commit(SET_ERROR, err.message); throw err; } finally { commit(SET_LOADING, false); } }, // 批量添加如从收藏夹一键加入 addItems({ commit }, items) { items.forEach(item commit(ADD_ITEM, item)); }, // 同步购物车从服务器拉取最新状态如用户在其他设备操作后 async syncCart({ commit, state }) { try { const serverCart await api.getCart(); // 服务端返回的是完整 cart 数组直接替换本地 state commit(REPLACE_CART, serverCart); } catch (err) { console.error(同步购物车失败, err); } } }; // 注意这里我们额外定义了一个 mutation 用于替换整个 cart // 因为服务端返回的 cart 可能包含本地没有的字段如优惠券信息 const REPLACE_CART (state, serverItems) { state.items serverItems.map(item ({ id: item.id, name: item.name, price: item.price, quantity: item.quantity, // 保留服务端特有字段 coupon: item.coupon })); }; export default { namespaced: true, state, getters, mutations: { ...mutations, REPLACE_CART }, actions };组件中调用示例商品卡片template div classproduct-card h3{{ product.name }}/h3 p¥{{ product.price }}/p button clickaddToCart :disabledisAdding {{ isAdding ? 添加中... : 加入购物车 }} /button /div /template script import { mapActions, mapState, mapGetters } from vuex; export default { props: { product: { type: Object, required: true } }, data() { return { isAdding: false }; }, methods: { // 映射 cart module 的 action ...mapActions(cart, [addItem]), async addToCart() { this.isAdding true; try { await this.addItem(this.product); } catch (err) { this.$message.error(err.message); } finally { this.isAdding false; } } } }; /script导航栏中显示数量全局组件template nav classheader router-link to/cart购物车 ({{ cartCount }})/router-link /nav /template script import { mapState, mapGetters } from vuex; export default { computed: { // 直接使用 cart module 的 getter ...mapGetters(cart, [totalCount]), cartCount() { return this.totalCount; } } }; /script这个例子证明Vuex 不是增加复杂度而是把隐式的、分散的、易出错的状态流转变成显式的、集中的、可测试的业务契约。当你需要新增“购物车分享”功能时只需在 cart module 中添加shareCartaction 和对应 mutation所有组件自动获得能力无需修改任何视图代码。4.2 持久化插件让购物车在刷新后依然存在Vuex 默认是内存状态页面刷新即丢失。但购物车、用户偏好等数据必须持久化。官方不提供内置方案但社区有成熟插件vuex-persistedstate。不过我更推荐手写一个轻量级持久化插件原因有三vuex-persistedstate会序列化整个 state若 state 包含函数、Date 对象、RegExp会导致JSON.stringify报错它默认持久化所有 state但你可能只想存 cart不想存 loading 状态手写插件能精准控制序列化/反序列化逻辑比如对 token 做加密存储。自定义持久化插件代码store/plugins/persist.js// store/plugins/persist.js export default function createPersistPlugin(options {}) { const { key vuex-store, // localStorage key paths [], // 需要持久化的 state 路径如 [cart.items, user.token] storage window.localStorage // 可替换为 sessionStorage 或自定义存储 } options; // 从 storage 加载初始 state let savedState {}; try { const json storage.getItem(key); if (json) { savedState JSON.parse(json); } } catch (e) { console.warn(Failed to load persisted state, e); } // 创建插件函数 return store { // 初始化将 savedState 合并到当前 store.state if (Object.keys(savedState).length 0) { // 递归合并只覆盖 paths 指定的路径 paths.forEach(path { const keys path.split(.); let target store.state; let source savedState; for (let i 0; i keys.length - 1; i) { target target[keys[i]]; source source[keys[i]]; } if (target source keys[keys.length - 1] in source) { target[keys[keys.length - 1]] source[keys[keys.length - 1]]; } }); } // 订阅 mutation当指定路径的 state 变更时保存到 storage store.subscribe((mutation, state) { // 只监听 paths 中的路径变更 for (const path of paths) { const keys path.split(.); let current state; for (const key of keys) { if (current typeof current object) { current current[key]; } else { break; } } if (current ! undefined) { // 该路径有值需要保存 try { const json JSON.stringify(savedState); storage.setItem(key, json); } catch (e) { console.warn(Failed to save persisted state, e); } break; // 找到一个就保存避免重复 } } }); }; }在 store/index.js 中使用import createPersistPlugin from ./plugins/persist; const store createStore({ // ...其他配置 plugins: [ createPersistPlugin({ key: my-shop-store, paths: [cart.items, user.token, app.theme] // 精确控制哪些数据持久化 }) ] });提示这个插件的关键优势在于paths参数。它允许你声明式地指定“只存 cart.items 数组不存 cart.loading 状态”避免把临时状态也写入 localStorage导致下次启动时 loading 一直为 true。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表从报错信息直击根源报错信息根本原因排查步骤解决方案TypeError: Cannot read property xxx of undefined在组件中访问了未定义的 getter 或 state 属性1. 检查mapGetters映射的名称是否拼写正确2. 检查该 getter 是否在对应 module 中定义3. 检查 module 是否开启了namespaced: true且映射时加了前缀使用mapGetters(moduleName, [getterName])或在 getter 中添加防御性判断state?.userInfo?.nameError: [vuex] do not mutate vuex store state outside mutation handlers.在 action、getter 或组件中直接修改了 state如state.count1. 全局搜索state.确认所有修改都发生在 mutation 内2. 检查是否误用了Object.assign(state, newData)应改为Object.assign(state, newData)仅适用于浅拷贝深层对象需用Vue.set严格遵守所有 state 修改必须在 mutation 中且只能通过state.xxx value或Vue.set(state, xxx, value)Unknown mutation type: xxxcommit 的 mutation 名称与 store 中定义的不一致1. 检查 mutation 名称是否全大写、下划线分隔2. 检查是否在 namespaced module 中却忘了加前缀如commit(cart/ADD_ITEM)3. 检查是否在 action 中误用了dispatch而非commit使用常量定义 mutation 名export const ADD_ITEM cart/ADD_ITEM在 commit 时commit(ADD_ITEM, payload)避免字符串硬编码store is not definedstore 未正确注入 Vue 实例1. Vue 2检查main.js中是否漏掉Vue.use(Vuex)2. Vue 3检查main.js中是否漏掉app.use(store)3. 检查 store 文件是否正确导出Vue 2 是export default new Vuex.Store({...})Vue 3 是export default createStore({...})在main.js中打印console.log(store)确认对象存在检查浏览器控制台是否有语法错误阻断执行getters are not reactive在 setup() 中直接访问store.getters.xxx未用computed包裹1. 检查是否写了const userName store.getters.userName错误2. 正确写法应为const userName computed(() store.getters.userName)所有对 getters 的访问必须包裹在computed中否则失去响应式5.2 我踩过的三个深坑与独家解决方案坑一在 mutation 中使用this导致 this 指向丢失现象mutation 函数里写了this.$http.get(...)运行时报错Cannot read property get of undefined。原因Vuex 的 mutation 是纯函数不绑定任何上下文this指向undefined严格模式下。解决方案绝对不要在 mutation 中访问this。API 调用必须放在 action 中mutation 只负责同步更新 state。如果确实需要在 mutation 中访问全局对象如 router应在 store 创建时通过插件注入// store/index.js import router from /router; export default createStore({ // ... plugins: [ store { // 将 router 注入 store 实例 store.router router; } ] }); // mutation 中 SET_ROUTE(state, route) { store.router.push(route); // 此时 store.router 可用 }坑二getters 中的异步操作导致无限循环现象在 getter 中写了async function getUser() { return await api.getUser(); }页面卡死。原因getter 必须是同步函数返回值会被 Vue 的响应式系统追踪。async函数返回 PromisePromise 对象没有响应式属性导致 Vue 无法建立依赖关系进而反复求值。解决方案getters 中禁止任何异步操作。需要异步数据必须在 action 中获取