十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vuex状态管理实战:从核心概念到性能优化

Vuex状态管理实战:从核心概念到性能优化 刚接手一个 Vue 项目时最容易忽略却又最影响后劲的往往不是组件写得多花哨而是状态管理体系拖了后腿。我见过不少项目前期用 props 和 $emit 传参攒起来很顺手等组件数量上了两位数、业务状态一多改一个值要顺着事件链追半天那感觉就像在乱麻里找线头。Vuex 的核心作用说白了就是给整个应用的共享状态找一个唯一可靠的管理者让数据流动变得有迹可循。这篇文章不是帮你复述官方文档而是从实际项目里的痛点和经验出发把 Vuex 的状态管理逻辑、模块化设计、常见坑位和性能优化思路完整拆开。适合正在学 Vue、准备引入 Vuex或者项目里已经用了但总觉得 store 越写越乱的朋友。我会尽量讲清楚每个设计背后的为什么而不是只丢给你一堆 API 用法。1. 为什么要用 Vuex状态管理的本质与组件通信的演进1.1 组件通信方案的演进与局限早期的 Vue 项目中团队之间的数据共享靠的是最原始的 props 向下传递和 $emit 向上通知。这在组件层级浅的时候没什么问题但一旦组件树变深问题就暴露了。举个实际场景一个页面顶部是用户信息栏中间是内容区内容区里又嵌套了一个操作面板操作面板需要读取顶层用户信息。如果用 props 一层层传每一层组件都要接收 userInfo 并再转发给子组件中间那些纯粹只是过路的组件也要跟着维护这些 props代码冗余不说一旦用户字段改名你得顺着整条链改下去漏一处就等着上线出 bug。事件总线EventBus是很多人尝试过的解法它确实解决了跨层级传递的问题但也带来了新的麻烦——全局事件没有结构约束项目一大谁都能往总线上挂事件谁都能监听想定位一个事件是哪里触发的、为什么触发追查起来非常痛苦。我接管过一个用了大量 EventBus 的项目线上反馈按钮点了没反应排查了半天才发现是某个模块在销毁时没有移除监听器导致回调被垃圾回收了。这种问题在 EventBus 模式下几乎无法避免。provide/inject 算是官方给的跨层级方案但它同样有一个明显的短板它只解决了把数据送下去的问题没有解决数据变更来源可追踪的问题。组件里直接改 injected 数据改完谁动了这份数据、什么时候动的完全无迹可寻后期维护依然靠猜。所以当项目里出现多个不相关的组件需要读写同一份数据时真正的诉求不是再找一种传参方式而是需要一个独立的、权威的数据仓库让所有共享状态从组件里抽离出来由专门的机制去管理读写。1.2 单向数据流为何解决了可维护性难题Vuex 设计的核心思想是单向数据流整个数据流动被严格限制在一条固定的链路里组件视图通过 dispatch 触发 actionaction 通过 commit 触发 mutationmutation 修改 statestate 变化后驱动视图更新。这条链路的每一步都是单向的、明确的。组件不能直接修改 store 里的 state至少在 strict 模式下会被警告所有状态变更都必须走 mutation。这样一来当页面上某个数据变了你只需要去 store 里看是哪个 mutation 被 commitpayload 是什么问题范围一下子就从整个项目缩小到了一个文件甚至一个函数里。还有一点特别关键mutation 被要求必须是同步的。这个约束看似多余其实是给 DevTools 的时间旅行调试打了基础。因为只有同步变更才能保证每次 commit 前后 state 的快照是可靠的你才能在调试面板里一步步回放状态的变化过程。如果 mutation 里混入异步操作前后的快照就会错位调试功能也就失去了意义。我在实际项目里最深的一个体会是Vuex 表面上引入了一套繁琐的流程但它换来的是所有变化都有迹可循的确定性。团队里新来的同事不用看文档顺着 dispatch 和 commit 的代码就能理解整个业务数据流这比任何交接文档都管用。1.3 什么时候才需要 Vuex当然Vuex 不是银弹也不该是无脑标配。如果你的应用只有两三个组件共享一个简单状态用 provide/inject 配合 reactive 也许就够了。引入 Vuex 意味着增加样板代码和心智负担为一个小功能搭建完整的 store 链路反而是过度设计。我判断一个项目是否需要 Vuex通常看三个条件第一多个互不相关的组件是否在读写同一份数据 第二这份数据的变更频率是否高、是否需要被严格追踪比如用户信息、权限、购物车 第三是否有跨页面、跨路由的持久性数据需要维护。如果三条里只满足一条可以再等等如果满足两条以上Vuex 就能真正发挥价值。中小型项目不必一上来就上 Vuex但如果你已经决定使用它就一定要从第一天开始按规范设计后面积累起来的状态多了再回头重构 store 的成本会很高。2. Vuex 五大核心概念深入拆解2.1 state单一数据源state 是 Vuex 的数据中心所有需要在组件间共享的状态都会注册在这里。它有一个很响亮的名头叫单一数据源意思是组件树里所有共享数据只有一个权威出处定义在 store.state 下的某个位置别的组件通过读取这个位置来获取数据。state 本身是响应式的。你可以在 Vue 组件里用this.$store.state.xxx直接访问也可以借助 mapState 把它映射到计算属性里。但在开发模式开启 strict 后不要在组件里直接修改 state否则控制台会报错。我在维护仓库时有个习惯state 对象在初始化时就尽量把字段定义完整后续再补充字段容易漏掉默认值导致某些组件拿到 undefined 时报错。一个标准的模块化 state 最好长这样const state () ({ userInfo: null, token: , permissions: [], loading: false })注意这里我用了函数返回的形式而不是直接写对象字面量。原因是模块可以被重复注册、复用如果 state 是同一个对象引用模块被复用或者做服务端渲染时状态就会互相污染。写成函数每次调用都会得到一份全新的状态对象这跟 Vue 组件 data 必须用函数是同一个道理。2.2 getters派生状态的缓存机制getters 相当于 store 的 computed它负责根据 state 派生出一些数据。举一个最常见的例子用户信息里存了出生日期列表页需要展示年龄你当然可以在每个组件里写一个计算函数但更好的做法是在 getters 里把这个派生逻辑统一收口getters: { userAge: state { if (!state.userInfo?.birthday) return null const birthYear new Date(state.userInfo.birthday).getFullYear() return new Date().getFullYear() - birthYear } }这样所有组件拿到的都是同一个派生逻辑的结果不会出现 A 组件算出来 27 岁、B 组件算出来 26 岁的尴尬差异。getters 的另一个特点是它可以读取其他 getters 的结果用法是第二个参数getters: { hasPermission: (state, getters) { return neededPermission getters.allPermissions.includes(neededPermission) } }当你需要给 getters 传入参数时就返回一个函数做法就像上面这样。但这里有一个需要留意的性能细节一旦 getters 返回了函数它就不会像普通 getters 那样做结果缓存了每次调用都会重新执行一遍计算。所以能不带参数就不带参数如果必须要带请确保函数内部的计算足够轻量。2.3 mutations唯一修改 state 的入口与同步约束mutation 是 Vuex 里唯一允许修改 state 的地方它定义成方法的形式第一个参数是 state第二参数是外部传入的 payloadmutations: { SET_USER_INFO(state, userInfo) { state.userInfo userInfo } }为什么非要多绕这一层不让组件直接改 state原因在于可追踪性。所有对 state 的改动都被强制收敛到有限的 mutation 方法里DevTools 才能记录每一次变更的类型和载荷你才能看到完整的变更历史。这份历史记录就是项目后期排错最重要的线索。mutation 必须保持同步这一点我在前面提过这里再强调一下为什么重要。如果 mutation 里发起了一个异步请求请求回来后再修改 state那么 DevTools 记录到的变更前状态和变更后状态就不在同一个时间切片上了时间旅行调试会变得不可靠。所以异步操作不要放在 mutation 里交给 action 去处理。还有个实操细节mutation 的 type 尽量用常量定义集中放一起管理。我在项目里会专门建一个mutation-types.js文件把模块内的 mutation 类型名都导出来。别小看这一步它带来的好处是第一编译期就能发现拼写错误第二在 IDE 里右键跳转定义很方便第三团队协作时大家不会因为字符串命名五花八门而产生分歧。严格模式下如果有组件直接改 stateVuex 会在控制台抛 warning。这个 warning 在开发期是宝贝它能在第一时间帮你发现不合规的代码但在生产环境strict 会造成额外的性能开销所以一般只建议在开发环境开启。2.4 actions异步操作与业务逻辑层action 是处理异步操作和业务逻辑的场所。它的函数签名是({ commit, dispatch, state, rootState }, payload) {}最常用的是 commit 来提交 mutationdispatch 来调用其他 action。一个很典型的场景是获取天气预报数据组件在 created 钩子里 dispatch 一个 fetchWeather 的 actionaction 内部通过 axios 请求接口数据回来后 commit 一个 SET_WEATHER 的 mutation把结果写入 state。整个过程组件不直接碰接口也不直接改数据它只负责下发一个指令后续的事情交给 action 去编排。actions: { async fetchWeather({ commit }, city) { commit(SET_LOADING, true) try { const { data } await http.get(/api/weather?city${city}) commit(SET_WEATHER, data) } finally { commit(SET_LOADING, false) } } }action 支持返回 Promise。dispatch 之后的链式调用可以轻松实现多个 action 的组合比如先登录再拉用户信息再初始化权限await dispatch(login, { username, password }) await dispatch(fetchUserInfo) await dispatch(fetchPermissions)这种组合方式让业务逻辑的编排变得非常直观。但要注意一个原则action 里不要直接修改 state。如果你在 action 里写了state.userInfo xxx在 strict 模式下一样会报错。action 的职责是处理流程真正的数据变更必须走 mutation。2.5 modules大型项目的模块化拆分当项目足够大时把所有的 state、mutations、actions、getters 全部塞进一个 store 文件里那个文件很快就会膨胀到几千行根本没法维护。modules 就是用来解决这个问题的。一个模块对象跟根 store 的结构一样有自己的 state、mutations、actions、getters。开发者可以按照业务领域来划分模块比如 user 模块管用户信息permission 模块管权限路由cart 模块管购物车。每个模块推荐开启namespaced: true。开启后模块内部的 mutation、action、getter 都会被自动注册到模块对应的命名空间下比如 user 模块里的SET_USER_INFO就变成了user/SET_USER_INFO这样不同模块之间哪怕出现同名的 mutation 类型也不会冲突。模块之间如果需要互相访问数据可以在 action 或 getter 里通过 rootState 和 rootGetters 访问根级别或兄弟模块的数据。举个例子permission 模块要根据 user 模块的登录状态来生成动态路由就可以这么写actions: { buildRoutes({ state, rootState }) { const isLogin rootState.user.token ! } }模块化之后每个模块的职责边界就清晰了。之前我在一个项目里看到开发者在全局 store 里同时维护用户、订单、商品、优惠券四个维度的状态找数据的时候要在几百行里翻来翻去后来拆成四个模块修改任何一个模块的业务都不会影响其他模块走查和 review 的效率明显提升。3. 构建高效可维护状态管理体系的实操指南3.1 目录结构设计从单文件到模块化一个可行的标准目录结构长这样src/store/ ├── index.js ├── mutation-types.js ├── modules/ │ ├── user.js │ ├── cart.js │ └── app.jsindex.js 负责创建 store 并注册所有模块import { createStore } from vuex import user from ./modules/user import cart from ./modules/cart import app from ./modules/app export default createStore({ modules: { user, cart, app } })这样根 store 只保留了模块的映射关系所有具体逻辑都下沉到对应模块文件里。每个模块文件的结构保持一致开发者在新增功能时只需要明确这个状态属于哪个业务域然后去对应模块里改不需要理解整个应用的全局状态。实际操作中我还会在模块文件内部保持固定的顺序先写 state再写 getters再写 mutations最后写 actions。这样一个文件读下来从上到下就是数据长什么样、派生数据怎么算、数据怎么改、异步流程怎么执行逻辑链条很清晰review 代码的时候省力不少。3.2 命名规范与代码组织命名规范看起来是鸡毛蒜皮的小事实际在团队协作里影响巨大。我见过有人在 mutation 类型里用setName有人用SET_NAME还有人直接写中文状态名混在一起简直灾难。我的做法是mutation 类型统一用大写字母加下划线比如SET_USER_INFO、SET_LOADINGaction 名用驼峰比如fetchWeather、updateUserProfile。mutation 类型统一放进mutation-types.js文件用export const导出。还有一点很重要一个 action 尽量只做一件事。比如 login 动作不要在一个 action 里既调用登录接口又拉取用户信息又初始化权限列表最好拆成多个独立的 action在组件里用 dispatch 按顺序组合。这样每个 action 都可以单独测试、单独复用排查问题时也更容易定位是哪个环节出了错。配合 ESLint 的 vue 推荐规则可以在代码提交阶段就拦截一部分常见的状态管理反模式比如禁止组件内直接修改 state。我们的做法是安装 eslint-plugin-vue并把 vue/no-mutating-props 这类规则加到 error 级别在 CI 里跑一遍不合规直接不让合并。3.3 通过 DevTools 提升调试效率Vue DevTools 是 Vuex 项目里最值得依赖的调试工具。打开浏览器开发者工具切到 Vuex 标签页你能看到完整的 state 树、所有已提交的 mutations 列表以及每一个 mutation 的 payload 快照。当页面出现数据异常时我的排查路径通常是先看 state 树里当前的数据值对不对再往前翻 mutations 记录找到最后一次修改该数据的 mutation 是哪一条再看它携带的 payload 是否符合预期。往往很快就能定位到是数据来源错了还是某个逻辑误触发了 mutation。时间旅行调试也是 Vuex 的一个亮点。在 DevTools 里点击任意一条历史 mutationstate 就会恢复到那个时间点的快照这对复现 bug、理解状态变化过程非常有帮助。不过要记得线上环境不要开启这个功能它会带来额外的状态快照存储开销。结合 strict 模式DevTools 会在控制台里直接显示组件直接修改了 state的警告这能帮助你守住只有 mutation 能改数据这条底线。3.4 与 Vue Router 协作登录状态与权限控制Vuex 和 Vue Router 是一对经典搭档最常见的协作场景就是登录状态管理和路由守卫。用户登录成功后把 token 和用户信息写入 store然后在 router.beforeEach 里读取store.state.user.token判断是否允许进入目标路由router.beforeEach((to, from, next) { const token store.state.user.token if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else { next() } })另一个常见需求是动态路由。后端接口返回当前用户可访问的路由列表后前端通过router.addRoute动态注册路由同时把路由信息存到 store 的 permission 模块里。这样用户刷页面时只要刷新接口能拿到数据就能重新渲染出完整的菜单和路由。这套流程里store 就像一个缓存中心把用户会话信息和权限数据集中管理router 只负责读取各司其职。这个场景就引出一个关键问题刷新页面时store 里是空的内存里的数据全部丢失。所以 token 等关键信息必须做持久化这个我在第 5 章会详细展开。4. 常见坑与排查实录4.1 watch 数组新旧值相同的陷阱很多人会遇到这个问题在组件里用 watch 监听 Vuex state 中数组的第一项发现新旧值打出来是一样的根本拿不到变化后的数据。原因其实不复杂。Vue 的 watch 默认比较的是引用而不是内部内容。如果你在 mutation 里直接操作了原数组比如state.list[0] newItem那数组本身还是原来那个引用watch 拿到的 newValue 和 oldValue 指向同一个对象自然打出来一样。解决办法是在修改数组或对象时总是返回一个新的引用。比如// 不推荐引用没有变化 mutations: { UPDATE_FIRST_ITEM(state, payload) { state.list[0] payload } } // 推荐返回新数组引用变化 mutations: { UPDATE_FIRST_ITEM(state, payload) { state.list state.list.map((item, index) index 0 ? payload : item) } }这个问题的本质跟 Vue 的响应式原理和 diff 机制有关。Vuex 里修改 state 时响应式系统能检测到变化并触发视图更新但如果 watch 想知道值变了它需要一个新的引用才能对比。所以我的经验法则是在 mutation 里尽量不修改原对象或原数组的内部属性而是生成一个新的值赋给 state。这不仅让 watch 更可靠也让 DevTools 的状态快照更准确。4.2 Maximum call stack size 错误的常见原因RangeError: Maximum call stack size这个错误在 Vuex 项目里出现通常意味着发生了无限递归。最常见的一种是在组件里 watch 了一个 state 字段watch 回调里又 dispatch 了一个 action而那个 action 又会被 mutation 修改成同一个字段于是再次触发 watch无限循环直到爆栈。我曾经在某次开发里误在 beforeCreate 钩子中 dispatch 了一个依赖于某个已经变化的 state 的 action结果触发了递归。排查办法很直接打开控制台的调用栈从最底层往上翻找到最早重复出现的那一层基本就是循环的根源。预防这一类问题可以记住两个原则第一watch 里不要轻易 dispatch 业务 action尤其是当你 watch 的状态本身可能被这个 action 修改时第二action 和 mutation 的职责要单一避免一个 action 又读又写同一份数据还触碰其他的状态。只要你遵守状态变更的通知和业务逻辑的触发解耦这个错误基本不会找上门。4.3 mapState 与 mapGetters 的使用细节mapState和mapGetters是简化组件内状态读取的利器。它们有两种常见写法数组写法适合直接映射同名状态对象写法适合做局部重命名或者做一点简单的计算// 数组写法 computed: { ...mapState([userInfo, token]) } // 对象写法重命名或取部分数据 computed: { ...mapState({ currentUser: state state.user.userInfo, isLoggedIn: state !!state.user.token }) }在 Vuex 4 Composition API 的组合式写法里mapState不能直接用于 setup因为它依赖组件上下文中的this.$store。正确做法是先用useStore()拿到 store 实例再配合computed手动取值或者借助computed把 mapState 的结果包一层import { useStore, mapState } from vuex import { computed } from vue const store useStore() const userInfo computed(() store.state.user.userInfo)如果你喜欢 mapState 的简洁风格也可以先用 store 实例接管映射再包成计算属性不过那样多一步封装我建议新写法里直接用 useStore 配合 computed 更直观。4.4 表单绑定与 v-model 的处理组件里有一个列表筛选条件你想把它直接 v-model 到 store 里的某个字段这看起来很省事但在 strict 模式下会直接把控制台刷爆。因为 v-model 默认是直接修改数据的它不经过 mutation。正规的思路有两种。第一种是使用 computed 的 get 和 setcomputed: { keyword: { get() { return this.$store.state.filter.keyword }, set(value) { this.$store.commit(SET_FILTER_KEYWORD, value) } } }这样在模板里写v-modelkeyword看起来跟普通绑定一样但实际上 setter 帮你把数据变更分散到了 mutation 里。第二种是走 action 流程本地维护一个临时数据用户确认或防抖后再 dispatch action 提交到 store。这种方式适合涉及异步逻辑的场景比如搜索关键词变化后自动请求接口action 里既更新了 state 又触发了接口请求流程更完整。两种方式各有适用场景核心思路是一致的组件不要直接碰 store 数据哪怕是最简单的表单输入也要让数据变更经过稳定的更新链路。4.5 常见问题速查表问题现象常见原因解决思路watch 数组/对象新旧值相同mutation 直接改了原引用用新数组/新对象替换原 stateDevTools 中 Mutation 列表看不到某些变更组件直接修改了 state开启 strict 模式强制走 mutation刷新页面后登录状态丢失store 是内存态持久化 token 和用户信息到 localStorageaction 异步结束后页面不更新action 里没 commit mutation或改了非响应式字段确保数据变更通过 mutation或在 state 初始化时声明字段模块里定义的 getter 全局可用没开启 namespaced每个模块设置namespaced: truev-model 绑定 store 字段报错直接修改 state 违规用 computed 的 get/set 或走 action 流程这张表我平时会贴在项目 Wiki 里团队新人遇到状态相关的问题先查一遍能省去很多重复答疑的时间。5. 性能优化与进阶实践5.1 大型项目中的模块懒加载对大型单页应用来说首屏渲染体积一直是需要关注的点。Vuex 提供了store.registerModule方法可以在运行时动态注册模块这样业务状态不需要在应用启动时全部加载。举个例子你的系统有一个数据分析页面只有进入这个页面才需要用到它的状态管理逻辑那你可以在组件 created 时动态注册对应模块import analysis from /store/modules/analysis onMounted(async () { if (!store.hasModule(analysis)) { store.registerModule(analysis, analysis) } })对应的在组件卸载时通过store.unregisterModule(analysis)注销模块避免内存泄漏和状态残留。注册和注销的时机需要你根据组件生命周期仔细设计。这个技巧尤其适合低频使用但状态复杂度高的业务场景。顺带提醒一句动态注册的模块如果 state 里包含敏感数据注销时一定要做好清理避免下一个用户误读到上一个用户残留的状态。5.2 状态持久化方案Vuex 默认是内存存储页面刷新就全没了。对于 token、用户信息这类需要跨刷新保留的状态持久化是绕不开的需求。最朴素的方案是在 mutation 里手动同步到 localStoragemutations: { SET_TOKEN(state, token) { state.token token localStorage.setItem(token, token) } }但这样每个模块都要写重复的同步逻辑维护成本偏高。更推荐的做法是写一个 Vuex 插件统一在 mutation 提交后自动把关键状态序列化到 localStorageconst persistPlugin (store) { store.subscribe((mutation, state) { const persistenceMap { token: auth_token, userInfo: auth_user } Object.keys(persistenceMap).forEach(key { if (mutation.type.includes(key.toUpperCase())) { localStorage.setItem(persistenceMap[key], JSON.stringify(state.user[key])) } }) }) }然后在创建 store 时传入插件const store createStore({ modules: { ... }, plugins: [persistPlugin] })页面刷新后在 store 创建之前读取 localStorage 里的数据初始化到 state 中。这样用户刷新后依然保持登录状态体验和原生 App 相差无几。持久化的时机也需要斟酌。频繁写入 localStorage 会带来性能问题我的习惯是在关键 mutation 提交后一次性写入而不是每次都写。如果数据量大考虑用节流或防抖或者干脆只持久化 token 和用户基本信息其他临时数据不落盘。5.3 Vuex 4 Composition API 的组合式写法Vuex 4 官方支持 Vue 3并且提供了 Composition API 风格的useStore函数。跟 Options API 相比组合式写法在代码复用和逻辑组织上更灵活。一个典型的 setup 内使用 Vuex 的例子import { useStore } from vuex import { computed } from vue export default { setup() { const store useStore() const userInfo computed(() store.state.user.userInfo) const isLoggedIn computed(() !!store.state.user.token) const login (payload) store.dispatch(user/login, payload) return { userInfo, isLoggedIn, login } } }在这种写法下store 的访问不再依赖this而是显式地通过 store 实例获取逻辑更清晰。如果你更习惯 Options API 里的 mapState也可以继续用只是要注意 mapState 在 setup 里需要额外处理。组合式 API 更适合复杂的业务组件它允许你把状态逻辑拆成可复用的 composable 函数比如把用户的登录态封装成一个useAuth()函数多个组件复用。5.4 Vuex 与 Pinia 的选型思考聊到 Vuex 不可能绕过 Pinia。现在新项目里越来越多团队直接上 Pinia它语法更简洁、对 TypeScript 支持更好、也没有 mutations 这层额外的概念。我个人的建议是新项目用 Pinia 没问题但存量 Vuex 项目不要轻易迁移迁移成本往往比想象中高而 Vuex 4 本身依旧稳定可靠。更关键的是无论用 Vuex 还是 Pinia状态管理背后的设计思想是一致的单一数据源、显式变更、模块化组织。你只要吃透了这些核心思想换一个状态库其实只是换 API 写法的问题。如果团队一定想迁移切记先梳理清楚项目里的 actions 和 mutations 边界不要在迁移过程中顺手优化业务逻辑那样很难保证旧功能的稳定性。迁移时尽量用小步快跑的方式按模块逐个替换每个模块替换完后跑一遍完整回归。6. 顺手用好 Vuex一个小技巧收尾最后分享一个我在团队里经常强调的小习惯store 里不要存放那些可以从别处派生出来的数据。比如用户头像如果 userInfo 里有头像地址就不要在另一个模块里单独存一份 avatar一旦两处数据不一致追查时头很疼。所有数据都应该有一个唯一的权威来源其他地方需要这份数据时要么读取 state要么通过 getters 派生而不是复制一份缓存起来。Vuex 的核心价值说到底不是有地方放数据而是它建立了一套纪律让数据流动变得有章可循。这套纪律的价值在项目只有 5 个组件时看不出来等团队有 10 个人协作、组件数量上百时你会发现它帮你省下的调试排查时间远远超过你建立这套体系时付出的成本。如果你还在为组件间数据剪不断理还乱而头疼不妨在今天写的代码里做一次收口把共享状态请进 store然后打开 DevTools 体验一次状态回放——那种所有变化尽在掌握的感觉是真的会上瘾。
返回列表