面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 05月31日 16:17

MobX 的核心概念是什么?它是怎么自动更新视图的?

MobX 解决的是一个很具体的问题:状态变了以后,哪些地方应该跟着变,不需要你手动列清单。它把应用里的数据看成一张依赖图,组件、计算值和副作用只要在运行时读过某个状态,就会被记录为这个状态的消费者。之后状态被修改,MobX 沿着这张图通知真正受影响的部分,所以它看起来像“自动更新”,实际靠的是运行时依赖追踪。MobX 的几个核心概念observable 是可观察状态,通常是对象属性、数组或 Map。它的关键不是“存数据”,而是让读写行为能被 MobX 捕获。computed 是从状态派生出来的值,比如 fullName、过滤后的列表、购物车总价。它默认惰性计算,只有被读取时才执行,并且依赖没变时直接复用缓存。这里的取舍很明显:computed 适合纯计算,不适合发请求、写日志这类副作用。action 是修改状态的边界。MobX 6 推荐把状态修改放在 action 里,因为 action 会批量提交变更,避免中间状态触发多次 reaction。团队项目里最好开启 enforceActions: "always",否则代码越写越散,很难追踪是谁改了状态。reaction / autorun / when 负责处理副作用。autorun 会立即执行并自动追踪用到的状态,reaction 可以精确指定观察的数据,when 在条件满足后执行一次就销毁。import { makeAutoObservable, configure } from 'mobx'configure({ enforceActions: 'always' })class TodoStore { todos: { id: number; text: string; done: boolean }[] = [] filter: 'all' | 'done' | 'active' = 'all' constructor() { makeAutoObservable(this) } get visibleTodos() { if (this.filter === 'done') return this.todos.filter(t => t.done) if (this.filter === 'active') return this.todos.filter(t => !t.done) return this.todos } addTodo(text: string) { this.todos.push({ id: Date.now(), text, done: false }) }}它为什么能自动更新视图在 React 里,observer 会包住组件渲染过程。组件渲染时读取了 store.visibleTodos,MobX 就知道这个组件依赖了对应的 computed;computed 又依赖 todos 和 filter。当 addTodo 或 filter 变化时,依赖链被标记为过期,组件才重新渲染。import { observer } from 'mobx-react-lite'export const TodoList = observer(({ store }: { store: TodoStore }) => ( <ul>{store.visibleTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>))边界也要清楚:MobX 只能追踪“运行时实际读取”的 observable。如果你提前把值解构成普通变量,再在组件外传来传去,依赖关系可能丢失。异步代码里也要注意,await 之后再修改状态,仍然需要在 action 或 runInAction 里完成。项目里怎么落地实际项目不建议把所有状态塞进一个全局 store。更稳的做法是按业务边界拆分,比如用户、权限、编辑器、购物车各自维护自己的状态,再在页面层组合使用。这样做的好处是更新范围小,测试也更容易写。边界是跨模块共享的数据不要随意互相 import,否则 store 之间会形成隐式依赖,后面重构很难拆。调试时可以配合 spy 或 MobX DevTools 观察 action 和 reaction,但不要把调试工具当成架构。真正能降低维护成本的,还是明确哪些方法能改状态、哪些 getter 只能派生数据。追问MobX 和 Redux 应该怎么选?MobX 更适合对象关系复杂、局部更新频繁的业务,比如表单编辑器、低代码画布、后台配置台。Redux 的优势是约束强,状态变更路径清楚,适合需要审计、回放和严格团队规范的项目。取舍点不在谁更先进,而在团队是否愿意用约束换可预测性。小团队快速迭代时 MobX 很舒服,但多人长期维护时要补上 action 规范和目录约定。computed 和普通函数有什么区别?普通函数每次调用都会重新执行,computed 会根据依赖做缓存。只有依赖变化并且有人读取它时,computed 才重新计算,这对列表过滤、聚合统计很有用。边界是 computed 必须保持纯净,不要在里面改状态或发请求。踩坑点是 computed 没有消费者时不会主动运行,所以不要指望它替你触发业务流程。autorun 和 reaction 有什么区别?autorun 适合“用到什么就追踪什么”的简单副作用,比如调试日志。reaction 更适合生产代码,因为它把数据选择和副作用分开,只在选择结果变化时触发。取舍是 autorun 写起来快,但依赖容易变得隐式;reaction 啰嗦一点,却更可控。项目里如果副作用会发请求或写本地缓存,优先用 reaction。使用 MobX 最容易踩什么坑?最常见的是把 observable 过早解构,导致 observer 组件没有在渲染阶段读取状态。另一个坑是异步请求回来后直接赋值,在严格 action 模式下会报错。边界处理方式是:组件里读 store,异步结果用 runInAction 合并回状态。还有一点,reaction 创建后要保留 disposer,在组件卸载或模块销毁时释放,否则会有隐性内存泄漏。MobX 适合所有状态吗?不适合。服务端缓存、分页请求、重试状态这类数据,用 TanStack Query 一类工具通常更合适。MobX 更适合客户端本地状态,尤其是用户正在编辑、拖拽、筛选、组合的状态。取舍上可以把远端数据交给请求缓存库,把前端交互状态交给 MobX,两者不要硬塞进同一个 store。
前端阅读 05月31日 15:55

MobX 和 Redux 到底该怎么选?适合哪些场景?

MobX 和 Redux 的区别不只是 API 写法不同,而是状态管理哲学不同。Redux 强调显式数据流:组件 dispatch action,reducer 生成新 state,状态变化可以被记录和回放。MobX 强调响应式模型:你修改 observable,系统自动知道哪些 computed、reaction 或 observer 组件需要更新。如果用一句话选型:需要强约束、审计和统一协作时偏 Redux;需要快速建模复杂业务对象、减少样板代码时偏 MobX。现在 Redux Toolkit 已经大幅减少模板代码,所以不能再简单说“Redux 一定啰嗦”。但 MobX 在深层对象、表单状态和局部复杂交互里仍然很顺手。// Redux Toolkitconst slice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { inc: state => { state.value += 1; } }});// MobXclass CounterStore { value = 0; constructor() { makeAutoObservable(this); } inc() { this.value += 1; }}Redux Toolkit 里看起来也能“直接改 state”,但那是 Immer 帮你生成不可变结果。MobX 的直接修改则是它本身的响应式模型,依赖追踪发生在读取和写入之间。两者都能写得很现代,真正影响选择的是团队调试方式、业务复杂度和长期维护成本。还有一个现实因素是招聘和交接成本。Redux 的资料、范式和候选人经验更多,新人即使没接触过项目,也容易顺着 action、slice、selector 找到入口。MobX 项目如果 store 设计得好,上手同样很快;如果设计得随意,新人需要先理解一套隐式依赖网络。选型时把团队未来一年的人数变化也算进去,往往比单纯比较代码量更实际。追问Redux 的优势现在还明显吗?明显,尤其是在多人协作和复杂状态审计场景里。Redux 的 action 日志、DevTools、时间旅行调试仍然很强,线上问题复盘时能看到状态如何一步步变化。取舍是你要接受更明确的流程和更多约束,哪怕 Redux Toolkit 已经减少了不少样板。金融、交易、权限流转这类系统,显式数据流带来的可追溯性通常比少写几行代码更重要。MobX 更适合哪些业务?MobX 适合状态结构像业务对象一样自然变化的场景,比如复杂表单、编辑器、看板、低代码配置器和局部交互很多的后台页面。它允许你用 class 表达领域模型,用 computed 表达派生值,用 observer 自动连接 UI。边界是自由度越高,团队规范越重要。若大家随手在任意位置改 observable,又不给 action 命名,后期排查会比 Redux 更痛苦。性能上 MobX 一定比 Redux 更好吗?不一定,但 MobX 的默认更新粒度通常更细。它追踪组件实际读取的 observable 字段,所以某个字段变化只影响真正用到它的组件。Redux 依赖 selector 和引用比较,写得好同样很快,写得差则容易因为新对象、新数组导致重复渲染。取舍在于 MobX 把优化自动化,Redux 把优化显式化;前者省心,后者更可控。TypeScript 项目选哪一个更舒服?MobX 的 class 模型和 TypeScript 搭配很自然,字段、方法、getter 的类型就是业务模型本身。Redux Toolkit 的类型体验也已经比旧 Redux 好很多,createSlice 能推断 action 和 state,但异步 thunk、RootState、Dispatch 仍然需要一些模板。取舍是 MobX 写业务模型更顺,Redux 写团队规范更统一。大型团队里,类型舒服不一定是唯一目标,统一的数据流和工具链也很值钱。能不能在一个项目里同时用 MobX 和 Redux?可以,但要非常克制。比如全局登录态、权限、审计相关状态放 Redux,某个复杂编辑器内部用 MobX 管局部模型,这是有边界的混用。踩坑点是没有划清职责,导致同一份数据在两个 store 里各存一份,最终同步逻辑比状态管理本身还复杂。除非收益明确,否则更建议选一个主方案,再用局部 React state 或轻量库补足边角。如果项目并不复杂,却又觉得 Redux 和 MobX 都偏重,也可以把 Zustand、Jotai、Valtio 这类轻量方案纳入比较。Zustand API 简单,适合轻量全局状态;Jotai 更偏原子化组合;Valtio 则接近可变对象代理的体验。这里的取舍是生态、团队熟悉度和调试能力,不要只看示例代码短不短。状态管理选型最怕为了“新”而换,最后业务复杂度没降,团队学习成本反而升了。落地时可以先画出状态的生命周期:哪些状态跨页面共享,哪些只服务某个复杂组件,哪些需要被审计或回放。跨团队、跨流程的状态更适合 Redux 这种强约束方案;局部领域模型、频繁编辑和深层对象更适合 MobX。这个判断比“哪个库更流行”靠谱,因为状态管理的问题通常不是 API 不够漂亮,而是边界没有定义清楚。所以 MobX 和 Redux 没有绝对胜负。Redux 像一套清晰的交通规则,MobX 像更灵活的自动导航;项目越重协作和审计,越需要规则,项目越重局部复杂交互,越能体现 MobX 的效率。
服务端阅读 05月31日 15:55

MobX 中 observable、computed 和 action 该怎么分工?

observable、computed 和 action 是 MobX 里最容易混在一起的三个词,但分工其实很清楚:observable 放状态,computed 放由状态推导出的值,action 放修改状态的过程。一个常见判断是,如果它需要被 UI 观察,就用 observable;如果它能由已有状态算出来,就别再存一份;如果它会改变状态,就让它进入 action 边界。import { makeAutoObservable, runInAction } from "mobx";class TodoStore { todos = [] as { title: string; done: boolean }[]; filter: "all" | "done" = "all"; constructor() { makeAutoObservable(this); } get visibleTodos() { return this.filter === "done" ? this.todos.filter(t => t.done) : this.todos; } add(title: string) { this.todos.push({ title, done: false }); }}上面 todos 和 filter 是 observable,visibleTodos 是 computed,add 是 action。使用 makeAutoObservable 时 MobX 会自动推断:字段变成 observable,getter 变成 computed,方法变成 action。它很省事,但不是没有边界;复杂 store 里仍然可以显式配置某些字段不追踪,避免把临时对象、第三方实例也变成 observable。这三个概念放到一起看,还能避免一个常见误区:不要为了“方便更新”把所有派生值都存成 observable。比如 totalPrice、selectedCount、isValid 这类值大多能从已有状态计算出来,存两份反而会带来同步问题。computed 的价值就在这里,它让数据源保持单一,同时又避免每次渲染都重复计算。makeAutoObservable 虽然好用,但在大型 store 里不要完全无脑依赖推断。某些字段可能只是缓存第三方实例,某些方法可能不应该自动绑定,某些深层对象也可能需要 observable.ref 这类更浅的追踪方式。选择默认推断还是显式 annotation,本质是开发效率和可控性的取舍。初期可以先自动推断,等 store 稳定后再把关键边界写清楚。测试时也能看出三者分工是否合理。observable 负责准备初始状态,action 负责触发业务行为,computed 负责断言结果是否正确。如果测试必须手动改很多中间值,往往说明 store 把派生值也当状态保存了,或者 action 颗粒度切得太碎。还有一个判断技巧:看这段代码是在回答“现在是什么”,还是“接下来做什么”。回答“现在是什么”的通常是 observable 或 computed,回答“接下来做什么”的通常是 action。比如 isSubmitDisabled 是当前表单状态的派生结论,应放 computed;submitForm 会校验、请求并写入结果,应放 action。边界清楚后,组件就不需要知道太多状态更新细节,只负责展示和触发行为。追问observable 是不是越多越好?不是,observable 应该只包会影响界面的业务状态。把所有对象都做成 observable 会增加理解成本,也可能让外部库实例被代理后行为变怪。取舍在于便利和边界:页面状态、表单值、接口数据适合观察,DOM 节点、WebSocket 实例、不可变配置通常不适合。踩坑点是把服务类、路由对象也塞进 store,后面调试时很难分清哪些变化真的应该触发 UI。computed 和普通函数有什么区别?computed 有缓存,并且会被 MobX 追踪依赖;普通函数每次调用都会重新执行。对于列表过滤、金额汇总、权限派生这类由 observable 算出的值,computed 通常更合适。边界是 computed 不适合接收经常变化的临时参数,也不应该包含副作用。实际项目里如果一个 getter 里顺手改了 loading 或发了请求,后续很容易出现循环触发。action 只是为了规范代码吗?action 不只是风格约束,它还提供状态修改的事务边界。一个 action 里连续改多个 observable,MobX 会等 action 结束后再通知 reaction,避免 UI 看到中间状态。取舍是你需要把“读”和“写”分清楚,不能在任何地方随手改 store。开启严格模式后,没放进 action 的修改会报错,这在团队协作时反而是好事。异步 action 应该怎么写才安全?async 方法可以作为 action 入口,但 await 之后的代码已经进入新的异步片段。为了让后续赋值仍然处在 action 边界内,可以用 runInAction 包住结果写入。这个写法比直接赋值多一点样板,但能避免严格模式问题,也能让状态变更集中。踩坑点是接口失败时只改了 error,忘记把 loading 改回 false,页面就会一直转圈。async loadTodos() { this.loading = true; try { const todos = await api.listTodos(); runInAction(() => { this.todos = todos; }); } finally { runInAction(() => { this.loading = false; }); }}三者在 React 组件里怎么配合?组件里应该尽量读取 observable 和 computed,而不是把派生逻辑散落在 JSX 中。按钮点击、表单提交这类事件则调用 action,让修改路径可追踪。取舍是 store 会稍微“厚”一点,但组件会更薄,测试和复用更容易。边界是不要把所有 UI 细节都塞进全局 store,弹窗开关、输入框临时草稿可以留在局部组件状态里。掌握这三者后,MobX 代码会变得很直观:状态放一处,派生值只计算不保存,修改都走明确的方法。真正要避免的不是 API 用错,而是把可观察状态、计算逻辑和副作用混在同一段代码里。
前端阅读 05月31日 15:55

MobX 依赖追踪到底是怎么知道该更新谁的?

MobX 依赖追踪的核心可以用一句话概括:谁在运行时读了 observable,谁就会被登记为它的依赖;以后这个 observable 变了,只通知登记过的人。这里的“谁”可能是 autorun、reaction、computed,也可能是被 observer 包装的 React 组件。MobX 不靠你手写依赖数组,而是靠运行时读取行为建立依赖图。一个最小例子如下。autorun 第一次执行时会读取 store.count,MobX 会把当前 reaction 和 count 这个可观察字段连起来。之后 count 改变,autorun 会重新执行;但如果改的是 name,这个 reaction 不会受影响。import { autorun, makeAutoObservable } from "mobx";class Store { count = 0; name = "MobX"; constructor() { makeAutoObservable(this); }}const store = new Store();const dispose = autorun(() => { console.log(store.count);});store.count++; // 触发 autorunstore.name = "Redux"; // 不触发上面的 autorundispose();从内部看,observable 字段像一个最小的发布者,reaction 像订阅者。运行 reaction 时,MobX 会设置一个“当前正在追踪的上下文”,字段 getter 被访问后就把这个上下文记录下来。执行结束后,旧依赖会被对比和清理,所以条件分支切换时,MobX 不会永远订阅已经不再读取的字段。依赖追踪还有一个经常被低估的边界:MobX 只追踪同步执行期间的读取。你在 reaction 里读到的 observable 会被记录,但 setTimeout、Promise 回调或事件监听器里后来才读到的字段,不会自动算进同一次 reaction 的依赖。这个规则解释了很多“明明在函数里用了状态,为什么没更新”的问题。数组、Map 和对象属性也有类似细节。读取 todos.length 追踪的是长度变化,读取 todos[0].title 追踪的是第一个元素及其 title,遍历整个数组则会建立更宽的依赖。依赖越宽,更新越容易触发;依赖越窄,性能越好但也更依赖你把读取位置写对。真实项目里,列表筛选建议放 computed,组件只读取最终结果,这样依赖和缓存都更清楚。最后要记住,自动追踪不是自动设计架构。MobX 能帮你找到“谁读了谁”,但不能替你决定 store 怎么拆、异步流程怎么收口、哪些状态应该全局共享。依赖图一旦跨模块乱连,短期很方便,长期会变成难以拆解的网。追问MobX 和 React 的依赖数组有什么区别?React 的 useEffect 依赖数组是你手动声明的,写漏了会闭包过期,写多了又可能重复执行。MobX 的依赖来自运行时读取,读了什么就追踪什么,不需要人工维护列表。取舍是 MobX 更省心,但依赖关系不总是一眼能从代码声明处看出来。团队里如果大量使用隐式读取,就要配合 trace、命名 reaction 和清晰的 store 边界。computed 为什么能缓存,什么时候会重新算?computed 本质上是一个有缓存的派生值,它会记住自己上次计算时读取了哪些 observable。只要这些依赖没有变化,再次读取 computed 会直接返回缓存结果。边界是 computed 必须是纯计算,不能在里面发请求、改状态或写日志埋点这类副作用。踩坑最多的是把带参数的筛选逻辑硬塞进 computed,参数变化不属于 observable 时,缓存行为就不符合预期。条件分支里的依赖会不会追踪错?不会静态追踪所有分支,只追踪当前这次运行实际访问到的字段。比如 showAge ? user.age : user.name,当 showAge 为 false 时,age 不会成为当前依赖。取舍是这让更新更精准,但也要求你理解“依赖是动态变化的”。如果条件切换后旧依赖没有清理,通常说明 reaction 没有重新执行,或者读取发生在了追踪上下文之外。为什么不建议在 reaction 里做太多事情?reaction 适合连接“状态变化”和“副作用”,但不适合承载大段业务流程。它会随依赖变化自动执行,逻辑过重时很容易出现重复请求、状态互相触发甚至循环更新。项目里的边界可以这样划:状态修改放 action,派生值放 computed,真正需要同步到外部系统时才用 reaction。踩坑点是忘记保存 dispose,页面卸载后 reaction 还活着,就会造成内存泄漏或幽灵请求。如何调试一个组件为什么重渲染?可以先在组件或 computed 中调用 trace(),观察 MobX 认为它依赖了哪些 observable。再看这些 observable 是不是在不该修改的时候被 action 改了,尤其是表单初始化、接口回填和路由切换。这个排查有个取舍:MobX 的自动追踪减少了手写优化,但问题出现时要从“读取链路”而不是“dispatch 链路”入手。实战中给 store action 起清楚的名字,比事后猜哪个字段触发更新要省很多时间。autorun(() => { trace(); console.log(store.visibleUserName);});MobX 的依赖追踪并不神秘,它只是把“读”和“写”都接管了。理解这一点后,很多问题都能归结为两个检查:状态有没有被观察,读取有没有发生在 reaction 执行期间。
服务端阅读 05月31日 15:55

React 里 MobX observer 为什么能自动更新组件?

MobX 的 observer 不是简单地给组件加一个订阅开关,它会在组件渲染时记录“这次 render 到底读了哪些 observable”。之后只有这些被读过的状态变化,组件才会重新渲染。也就是说,observer 的关键不是“组件用了 store”,而是“组件在渲染期间访问了 store 的哪个字段”。在 React 项目里,函数组件通常使用 mobx-react-lite,类组件才会用到 mobx-react。MobX 6 以后更推荐 makeAutoObservable,少写装饰器,也更适合 TypeScript 和现代构建环境。import { makeAutoObservable } from "mobx";import { observer } from "mobx-react-lite";class CounterStore { count = 0; name = "MobX"; constructor() { makeAutoObservable(this); } inc() { this.count += 1; }}const store = new CounterStore();export const Counter = observer(() => { return <button onClick={() => store.inc()}>{store.count}</button>;});上面组件只读取了 store.count,所以 store.name 改变不会让它重渲染。这个粒度比“整个 store 变化就刷新”要细很多,也是 MobX 在复杂表单、局部状态很多的页面里比较省心的原因。实际接入时,还要想清楚 store 从哪里来。小 demo 里直接 const store = new CounterStore() 没问题,但真实应用通常会用 React Context 注入 store,避免测试、SSR 或多实例页面互相污染。尤其是 Next.js 这类服务端渲染场景,全局单例可能把 A 用户的状态带到 B 用户请求里,这是很隐蔽的边界问题。另一个容易忽略的点是 React 18 的 StrictMode。开发环境下某些渲染和 effect 会被重复调用,用来暴露副作用问题,很多人会误判为 observer 重复更新。判断时要区分“React 开发模式故意重复执行”和“MobX 依赖真的变化”。如果 action 里混入请求、埋点或一次性初始化逻辑,最好把这些副作用放到明确的生命周期或事件里,不要依赖 render 触发。还有一个实用经验:不要过早把 store 解构成一堆局部变量再传来传去。const { count } = store 在某些位置只是拿到了当前值,后续组件读取的就不是 observable getter。保留 store.count 的读取路径,或者在 observer 子组件内再读取,通常更符合 MobX 的追踪模型。如果使用 Context 注入 store,也不要在 Provider 的 render 里反复 new Store()。每次创建新实例都会让依赖关系重建,旧组件里的 reaction 也可能来不及按预期清理。通常可以用 useState(() => new Store()) 或模块级工厂保证实例生命周期稳定。这个细节不显眼,但在多标签页、弹窗复用和测试隔离时很容易变成偶发 bug。追问observer 应该包在父组件还是子组件上?更推荐把 observer 放在真正读取 observable 的叶子组件上,而不是一股脑包住最外层页面。这样依赖会更小,某个字段变化时只刷新用到它的那块 UI。取舍是组件数量会多一些,但性能边界更清晰,排查“为什么这里更新了”也更容易。踩坑点是父组件先把 observable 解构成普通值再传下去,子组件即使包了 observer,也可能失去追踪效果。为什么有时数据变了,observer 组件却不更新?最常见原因是渲染期间没有直接读取 observable,例如在组件外提前把值存成普通变量。MobX 只能追踪 reaction 执行时发生的读取,追踪不到已经脱离 observable 的快照。另一个边界是对象本身没有被 makeAutoObservable、observable 或对应 annotation 处理,普通对象当然不会触发更新。实际项目里还要检查状态修改是否发生在 action 中,开启 enforceActions 后,违规修改会直接暴露问题。observer 和 React.memo 需要一起用吗?多数情况下不需要,observer 本身已经对 observable 依赖做了细粒度判断,也会处理一部分 props 变化带来的重复渲染。React.memo 更适合纯展示组件,用普通 props 驱动且没有读取 MobX 状态。两者强行叠加不一定出错,但容易让团队误以为“性能优化越多越好”。真正需要权衡的是组件边界:把读取状态的组件拆小,通常比到处加 memo 更稳定。在 render 里创建新对象会影响 observer 吗?会影响,但影响点通常不是 MobX 追踪,而是 React 子组件的 props 比较。比如每次 render 都创建 { color: 'red' } 或新的回调函数,传给普通子组件时可能导致子组件跟着刷新。这个坑在 MobX 页面里更隐蔽,因为你会以为是 observable 更新太频繁,其实是 React 引用变化。固定样式对象、用 computed 产出派生数据,或者把子组件也拆成 observer,都是可选方案。异步请求里修改状态,observer 会怎样更新?异步本身不会破坏 observer,关键是每次修改 observable 时是否在 action 边界内。async 函数里 await 之后已经离开原来的同步 action,因此后续赋值最好放进 runInAction 或拆成单独 action。这样做的取舍是代码多几行,但状态变化会更可追踪,也能避免严格模式报错。多个字段一起更新时,action 还能合并通知,减少组件中间态闪烁。async load() { this.loading = true; const data = await api.getList(); runInAction(() => { this.items = data; this.loading = false; });}observer 用得好,核心不是“所有组件都包一下”,而是让组件在正确的位置读取正确的 observable。状态读取越靠近展示位置,MobX 的自动追踪越准确,页面也越不容易出现莫名其妙的刷新。
服务端阅读 05月31日 15:55

MobX 异步操作为什么要用 runInAction 或 flow?

MobX 处理异步的核心问题只有一个:await 之后,代码已经离开了原来那个 action 的同步执行栈。如果项目开启了 enforceActions,这时直接改 observable 可能报错,调试也会变乱。所以 MobX 异步写法通常有两条路:普通 async/await 搭配 runInAction,或者使用 flow 让 generator 的每一步自动包进 action。async/await 的常见写法简单请求用 runInAction 最直观。请求前设置 loading;请求回来之后再改 data、error、loading,就放到 runInAction 里。这样状态变更集中,组件只看到清晰的开始、成功或失败、结束。class UserStore { user = null; loading = false; error = null; constructor() { makeAutoObservable(this, {}, { autoBind: true }); } async fetchUser(id) { this.loading = true; this.error = null; try { const user = await api.getUser(id); runInAction(() => { this.user = user; }); } catch (e) { runInAction(() => { this.error = e.message; }); } finally { runInAction(() => { this.loading = false; }); } }}flow 适合更长的流程flow 用 generator 写异步,yield 后面的状态修改仍由 MobX 管理。它适合串行步骤多、需要取消任务、错误分支复杂的场景。代价是语法不如 async/await 普及,TypeScript 类型也要多处理一点,团队不熟时会增加理解成本。fetchUser = flow(function* (id) { this.loading = true; try { this.user = yield api.getUser(id); } catch (e) { this.error = e.message; } finally { this.loading = false; }});并行请求和重试都要考虑“最后一次结果才有效”。否则慢请求可能覆盖快请求,页面显示旧数据。常用做法是记录 requestId,或使用 AbortController 取消旧请求。追问为什么 @action async 函数里 await 后还要 runInAction?@action 包住的是函数开始执行的同步部分,不会自动把所有未来的 Promise 回调都包住。await 后恢复执行时,MobX 已经不能保证这段修改仍在 action 中。runInAction 的取舍是多写一点样板代码,换来严格模式下可预测的状态修改。坑在于本地没开 enforceActions 时看不出问题,线上团队规范一收紧就开始报错。runInAction 和 flow 应该选哪个?短流程、团队熟悉 async/await 时,用 runInAction 更自然。流程很长、每一步都要改状态,或者希望使用 MobX 自带的取消能力时,flow 更顺手。边界不是哪个更高级,而是代码是否还能一眼看出状态变化发生在哪里。大型团队里最好统一风格,否则同一个项目两套写法会增加维护成本。loading 状态为什么容易写错?最常见的坑是并发请求共用一个 boolean。第一个请求还没结束,第二个请求开始;第二个先结束把 loading 设 false,页面就误以为全部结束了。可以用 requestId 保证只处理最后一次请求,或用 pendingCount 表示还有几个请求未完成。取舍是代码稍复杂,但能避免旧请求覆盖新结果和 loading 闪烁。const id = ++this.requestId;const data = await api.search(keyword);runInAction(() => { if (id === this.requestId) this.results = data;});异步错误应该放在 store 里还是组件里?业务错误通常放 store 里,因为多个组件可能都要根据它展示状态或禁用按钮。只影响某个弹窗的一次性错误,可以留在组件本地,没必要污染全局 store。边界是错误是否属于业务状态:比如“用户未登录”是业务状态,“当前弹窗输入为空”更像 UI 状态。踩坑点是把所有 error 都塞进全局数组,最后用户看到的提示和真实操作对不上。组件卸载时异步任务要不要取消?如果请求结果只会写入全局 store,组件卸载不一定必须取消,但要防止过期结果覆盖新状态。若结果只服务当前组件,卸载时取消更稳,尤其是搜索框、详情页切换和轮询。flow 返回的任务可以 cancel,fetch 可以用 AbortController。边界在于:取消不是为了让 MobX 安全,而是为了避免无意义请求和过期数据写回。
服务端阅读 05月31日 15:55

大型前端应用中 MobX Store 应该如何拆分和协作?

大型应用里用 MobX,关键不是“建一个 store 然后全局导出”,而是把状态边界划清楚。页面越多,store 越容易变成杂物间:登录信息、列表缓存、弹窗状态、错误提示、持久化逻辑全塞一起,短期写得快,后期任何改动都像拆炸弹。比较稳的做法是用 RootStore 管组合关系,业务 Store 管自己的数据和动作,跨模块协作只通过少数明确入口发生。推荐的基本结构RootStore 不应该写太多业务逻辑,它更像装配层。UserStore、ProductStore、CartStore 等模块各自维护领域状态,构造时拿到 root 或必要依赖。也方便测试时替换依赖。class RootStore { userStore: UserStore; cartStore: CartStore; constructor(api: ApiClient) { this.userStore = new UserStore(api.user); this.cartStore = new CartStore(api.cart, this.userStore); makeAutoObservable(this, {}, { autoBind: true }); }}class CartStore { items: CartItem[] = []; constructor(private api: CartApi, private userStore: UserStore) { makeAutoObservable(this, {}, { autoBind: true }); } get total() { return this.items.reduce((s, i) => s + i.price * i.count, 0); }}哪些状态该进 MobX长期共享、会被多个页面读取、需要派生计算的状态适合进 MobX,比如用户、权限、购物车、筛选条件、实体缓存。只属于一个组件的输入框临时值,不必为了“统一管理”强行进全局 store。取舍点在生命周期和共享范围:短命状态适合本地,长期状态适合 store。持久化也不要一把梭。登录 token、主题、语言可以落 localStorage;大量实体数据更适合接口缓存。autorun 自动保存很方便,但要防抖、过滤敏感字段,并在 store 销毁时清理 disposer。追问单一 RootStore 和多个业务 Store 怎么取舍?小项目用一个 store 没问题,文件少、调试直接。大型应用更适合多个业务 Store,因为团队可以按领域并行维护,测试也更容易隔离。代价是跨 store 依赖会变复杂,所以 RootStore 要控制装配关系,不能让每个 store 随意 import 另一个单例。边界是:当一个文件开始同时处理三个以上业务域时,就该拆了。Store 之间通信能不能用事件总线?能用,但一般不推荐作为首选。事件总线让依赖看起来松,实际调试时很难知道某个状态是谁改的。更稳的方式是构造函数注入依赖,或者由上层 action 编排多个 store。事件总线适合埋点、全局通知这类不要求强业务一致性的场景,别拿它处理订单和权限这种核心流程。computed 越多性能越好吗?不是,computed 适合表达由 observable 推导出的稳定结果,并且只有被观察时才有缓存价值。把所有函数都改成 computed 会让依赖关系变难读,还可能因为返回新数组导致下游重复渲染。大型列表里常见做法是把筛选条件和原始数据分开,computed 只做必要派生。边界是:它应该没有副作用,也不应该偷偷发请求或修改状态。get visibleProducts() { return this.products.filter(p => p.category === this.category);}大型应用如何避免 Store 变成上帝对象?先按业务能力拆,而不是按 observable、action、computed 这种技术类型拆。比如 user、order、payment 这样的领域边界,比“actions 文件夹里塞一切”更容易维护。每个 store 暴露少量动作,内部字段能私有就私有,组件不要绕过 action 到处改。踩坑点是为了省事把 root 传给所有 store,最后每个 store 都能访问全世界。MobX Store 怎么测试才不脆?优先测试业务动作和派生结果,不要测试 MobX 内部实现。API 依赖用 fake client 注入,异步 action 等待结束后断言 observable 状态即可。组件测试只验证 UI 是否响应 store 状态变化,没必要把真实 RootStore 全挂上。边界在于:如果一个测试需要同时准备五个 store 才能跑,通常说明业务逻辑拆分还不够清楚。
前端阅读 05月31日 15:55

MobX 的 toJS、toJSON 和 observable.shallow 到底该怎么选?

MobX 里经常让人混淆的不是 observable 本身,而是数据离开 MobX 时该怎么处理。toJS、toJSON 和 observable.shallow 都和“普通对象”有关,但位置不同:toJS 负责把响应式数据深度转成普通对象,toJSON 负责控制序列化输出,observable.shallow 则是在创建 observable 时只追踪顶层。## 先记住一句话把 MobX 数据交给 API、缓存或第三方库时,用 toJS;控制 JSON.stringify 输出时,写自定义 toJSON;数据量很大且整批替换时,才考虑 observable.shallow。三者不是替代关系。import { makeAutoObservable, observable, toJS } from 'mobx';class UserStore { profile = { name: 'Alice', meta: { role: 'admin' } }; rows = observable.shallow.array(); constructor() { makeAutoObservable(this, { rows: observable.shallow }); } save() { return api.updateUser(toJS(this.profile)); } toJSON() { return { profile: toJS(this.profile) }; }}三者的边界在哪里toJS 会递归剥离 observable,新对象不再被 MobX 追踪。它适合提交表单、生成快照、写 localStorage。不要在 render 或 computed 里反复调用。toJSON 不是 MobX 的深转换工具,更多时候是 JavaScript 的序列化钩子。JSON.stringify(obj) 会自动调用对象自己的 toJSON。它适合做字段白名单、脱敏和输出格式控制。observable.shallow 不负责导出数据,它只改变 observable 的创建策略。默认 deep 会追踪嵌套对象,shallow 只看顶层引用。大型表格、分页结果适合它;可编辑表单通常不适合。追问为什么不建议在 React render 里直接 toJS?因为 toJS 是深拷贝,不是只读一下引用。组件每次响应式更新都会生成新对象,子组件的浅比较、memo 和虚拟列表优化都会被破坏。它的取舍是换来干净对象,但放弃引用稳定性和部分性能。边界处用一次很合理,渲染路径里反复用就是踩坑。JSON.stringify observable 时还需要先 toJS 吗?多数普通场景不需要,MobX observable 通常可以被 JSON.stringify 序列化。真正需要 toJS 的情况,是你要先加工数据、交给只接受 plain object 的库,或明确断开响应式引用。另一个边界是敏感字段,这时自定义 toJSON 更合适。坑在于以为 toJSON 会自动深度剥离所有 observable,实际它只按你的返回值工作。observable.shallow 为什么会导致 UI 不更新?shallow 只追踪顶层引用,store.config.theme = 'dark' 没换掉 config 引用,所以 MobX 不会通知观察者。正确写法是替换整个顶层对象:store.config = { ...store.config, theme: 'dark' }。它的好处是减少深层追踪开销,代价是必须使用不可变更新风格。团队里如果有人习惯直接改嵌套字段,就很容易出现数据变了但页面没动。store.config.theme = 'light';store.config = { ...store.config, theme: 'light' };大列表一定要用 observable.shallow 吗?不一定,要看列表里的元素会不会被单独编辑。如果列表只是接口返回后展示、筛选、整页替换,shallow 很合适。如果每一行都有选中、编辑、校验状态,deep 或拆成行级 store 更稳。边界判断很简单:业务是否关心元素内部字段变化触发 UI。toJS 得到的数据能再改回 store 吗?可以,但要把它当快照,不要当双向绑定对象。toJS(store.user) 改出来的 plain object 不会影响原 store,想写回必须在 action 里显式赋值。这个边界在表单草稿里很有用:先复制一份编辑,保存时再提交。踩坑点是把快照传来传去后误以为还在响应式系统内,结果修改没有任何观察者收到通知。
前端阅读 05月31日 15:55

MobX 应用应该怎么测试 Store 和组件?

测试 MobX 应用时,重点不是验证“observable 是否真的能响应”,而是验证业务状态在 action、computed、reaction 和组件渲染之间有没有按预期流动。优先测试 Store,因为大部分规则都在那里;组件测试只覆盖用户能看到和能操作的行为。API 边界可以用 mock 或 MSW 隔离,reaction 这类副作用要记得释放 disposer。不要把每个 observable 字段都测一遍,那会让测试和实现细节绑死,重构一次就碎一片。一个比较稳的分层是:Store 单元测试覆盖状态变化和异常分支,组件测试覆盖点击、输入、展示,少量集成测试覆盖多个 store 的协作。MobX 的响应式机制本身已经由框架保证,业务测试没必要重复证明它存在。真正容易出问题的是异步 action 的 loading 恢复、错误消息、reaction 泄漏、组件没被 observer 包裹,以及 mock 数据和真实接口结构不一致。class UserStore { user = null; loading = false; error = ''; constructor(private api) { makeAutoObservable(this); } get isLoggedIn() { return Boolean(this.user); } async login(payload) { this.loading = true; try { this.user = await this.api.login(payload); } catch (e) { this.error = e.message; throw e; } finally { this.loading = false; } }}追问Store 测试应该测状态字段还是业务行为?优先测业务行为,也就是调用 action 之后,对外可观察的状态和 computed 结果是否正确。直接断言每个内部字段会让测试过度依赖实现,比如后来把两个字段合并成一个对象,功能没坏但测试全红。边界是错误状态、loading、权限标记这类会直接影响 UI 的字段,仍然应该明确断言。写法上可以把 store 当成普通类实例,不需要为了测试专门启动 React。取舍是测试会少覆盖一些内部细节,但更能支持后续重构。MobX 的异步 action 有哪些测试坑?最大的坑是没有等待异步结束就断言,导致测试偶发通过或失败。await store.login() 后再检查 user、loading、error,比用固定 setTimeout 稳得多。另一个坑是错误分支只测抛错,不测状态恢复,结果线上 loading 一直转。异步 action 里如果用了 runInAction,测试仍然关注最终行为,不必断言 MobX 的内部调度顺序。边界是包含防抖、轮询或取消请求的 action,需要同时控制 timer 和请求 mock,否则失败会很随机。组件测试里要不要 mock 整个 Store?如果目标是验证组件交互,mock 一个最小 Store 很有用,速度快,也能把失败范围限制在组件。若要验证多个 store 协作、路由跳转或真实 reaction,就应该使用接近真实的 store 实例,甚至配合 MSW mock 网络。过度 mock 的问题是组件看起来测过了,但和真实 store 的 observable 更新方式不一致。一个实用取舍是:单组件用轻量 mock,关键业务流用真实 store 加 mock API。还要注意组件必须在测试里保持和生产一致的 Provider 结构,否则通过的测试可能只是绕开了依赖注入问题。reaction、autorun 这类副作用怎么测?先触发会改变依赖的 action,再断言副作用是否发生,例如调用埋点、写 localStorage 或更新另一个 store。测试结束时一定要执行 disposer,否则后续用例可能被上一个 reaction 影响。这里的边界是不要测试 MobX 会不会追踪依赖,应该测试你注册的副作用是否符合业务预期。若副作用包含防抖或定时器,需要配合 fake timers,并在用例后恢复真实 timer。踩坑点是 reaction 默认可能立即执行,也可能等依赖变化才执行,测试前要明确当前配置。spy、trace、isObservable 适合放进断言吗?spy 和 trace 更适合排查响应链路,不建议长期作为核心断言,因为它们会让测试贴着 MobX 实现细节跑。isObservable 可以在库代码或 store 工厂里做少量保护,但业务测试里通常没必要。真正有价值的断言是用户状态、组件输出和副作用结果。踩坑点是为了提高“测试覆盖率”去检查 observable 类型,最后覆盖率上去了,坏业务却没被测到。取舍上,调试工具可以临时打开,但最终测试最好回到业务可见结果。写段测试it('logs in and resets loading', async () => { const api = { login: jest.fn().mockResolvedValue({ name: 'Ada' }) }; const store = new UserStore(api); await store.login({ username: 'ada' }); expect(api.login).toHaveBeenCalledWith({ username: 'ada' }); expect(store.user.name).toBe('Ada'); expect(store.isLoggedIn).toBe(true); expect(store.loading).toBe(false);});MobX 测试的主线很简单:Store 测规则,组件测行为,reaction 测副作用,API 边界用可控替身。只要少测实现细节,多测状态变化带来的可见结果,测试既能挡住回归,也不会在重构时变成负担。遇到测试不稳定时,先检查异步等待、disposer、timer 和 mock 清理,通常比怀疑 MobX 本身更接近答案。
前端阅读 05月30日 01:39

MobX 中 observable 怎么用?有哪些注意事项?

MobX 的 observable 用来把普通状态变成“可被追踪的状态”。组件、computed、autorun 或 reaction 读取它时,MobX 会记录依赖;之后状态变化,相关派生值和视图就会自动更新。现在项目里更常用 makeAutoObservable 或 makeObservable,装饰器写法能见到,但要看团队 Babel/TypeScript 配置。注意:修改状态最好放在 action 里,大对象可用 shallow 降低追踪成本。追问makeAutoObservable 和 makeObservable 有什么区别?makeAutoObservable 会按成员类型自动推断:字段是 observable,getter 是 computed,方法通常是 action。makeObservable 需要手动标注,麻烦一点,但控制更精确,适合复杂 store。observable 默认是深度追踪吗?是的,普通对象会递归转成可观察结构。数据很大、嵌套很深,或者只关心引用变化时,可以用 shallow,避免不必要的代理和依赖追踪。为什么建议在 action 中修改状态?action 能把多次修改合并成一次事务,减少中间状态暴露,也方便开启 enforceActions 做约束。异步请求完成后修改 observable,常用 runInAction。observable 和 computed 怎么配合?observable 存原始状态,computed 负责派生结果。比如 todos 是 observable,completedTodos 应该是 computed,而不是每次在组件里重复 filter。项目里常见坑是什么?一是解构 observable 后丢失响应式读取场景;二是把 observable 对象直接传给不支持代理的第三方库;三是冻结对象或随意替换深层结构,导致追踪和更新不符合预期。写段代码class TodoStore { todos = [] filter = 'all' constructor() { makeAutoObservable(this) } addTodo(text) { this.todos.push({ text, done: false }) } get doneTodos() { return this.todos.filter(t => t.done) }}
前端阅读 05月30日 01:39

MobX 中 computed 有什么作用?和 reaction 怎么选?

MobX 的 computed 用来声明“由 observable 推导出来的值”,比如过滤后的列表、总价、表单是否有效。它的关键点是自动追踪依赖、懒计算、缓存结果:没人读取时不算,依赖没变时重复读取也不重算。面试回答要强调:computed 应该像纯函数,只负责返回值,不要发请求、写日志或修改状态;这些副作用应该交给 reaction。追问computed 为什么能提升性能?因为它会缓存上一次计算结果。只有依赖的 observable 变化,并且 computed 再次被读取时,MobX 才会重新计算;复杂过滤、排序、聚合都适合放进去。computed 和普通 getter 有什么区别?普通 getter 每次访问都执行。computed getter 会被 MobX 管理依赖和缓存,在 observer、autorun、reaction 等响应式上下文中效果最明显。computed 里能不能写异步请求?不建议,也不应该。computed 要同步返回派生值;异步请求会产生副作用,应该用 action 改状态,再用 computed 读取状态生成结果。computed 和 reaction 怎么选?要“算出一个值”,选 computed;要“值变了以后做一件事”,选 reaction。比如 completedTodos 是 computed,userId 变化后拉接口是 reaction。项目里有什么坑?不要在 computed 里返回每次都新建且结构相同的对象,否则可能让观察者误以为结果变了。需要时可以拆小 computed,或使用结构比较配置。写段代码class TodoStore { todos = [] filter = 'all' constructor() { makeAutoObservable(this) } get visibleTodos() { if (this.filter === 'done') return this.todos.filter(t => t.done) return this.todos }}
前端阅读 05月30日 01:39

MobX 中 autorun、reaction 和 when 有什么区别?

MobX 里的 reaction 用来处理副作用:状态变了以后,去做日志、请求、持久化、路由跳转这类“不产生派生值”的事。常见有三种:autorun 会立即执行并自动追踪用到的 observable;reaction 把“追踪什么”和“执行什么”分开,更适合精确控制触发条件;when 只在条件第一次满足时执行一次,然后自动清理。面试里要先说清:派生数据用 computed,副作用才用 reaction。追问autorun 和 reaction 有什么区别?autorun 会立即跑一次,函数里读到什么 observable 就追踪什么。reaction 先用 data 函数明确返回要追踪的数据,只有这个数据变化时才执行 effect,适合监听 userId、query 这类明确字段。when 适合什么场景?适合“一次性条件触发”,比如用户登录成功后加载资料、初始化完成后启动订阅。它触发一次后会自动 dispose,不适合长期监听。reaction 里最容易踩什么坑?一是忘记清理 disposer,组件卸载后还在监听;二是在 reaction 里修改自己依赖的状态,造成循环触发;三是异步请求回来后没用 runInAction 修改状态。reaction 和 computed 怎么选?要返回可缓存的派生值,用 computed;要调用接口、写 localStorage、打印日志、操作外部系统,用 reaction。一个记忆法是:computed 回答“值是什么”,reaction 回答“变化后做什么”。写段代码const dispose = reaction( () => store.query, query => { if (query.length > 2) store.search(query) }, { delay: 300 })// React 卸载或不再需要时// dispose()
服务端阅读 05月30日 01:39

React 中如何正确使用 MobX 和 observer?

在 React 中用 MobX,核心是让读取 observable 的组件被 observer 包住。store 可以通过 Context、props 或模块变量传入;实际项目更推荐 Context,测试和多实例更好控。observer 会追踪组件渲染时真正读到的 observable,相关字段变化才重渲染,所以不要在外层提前把 observable 解成普通值再传下去。函数组件优先用 mobx-react-lite,类组件或旧项目才考虑 mobx-react。追问observer 应该包父组件还是子组件?谁读取 observable 就包谁。把整个 App 包起来不等于所有子组件都响应,细粒度 observer 反而更容易减少无关渲染。Context 里的 store 要不要经常替换?通常不要。Provider 的 value 保持同一个 store 实例,更新 observable 字段即可;频繁替换 store 会让依赖关系和测试都变复杂。组件为什么没有更新?优先查三件事:组件是否用了 observer,读取的对象是否真是 observable,是否在 observer 组件外提前解构成普通值。第三方组件能直接吃 observable 吗?不建议。第三方组件不是 observer,传入前最好转成普通数据,或只传它需要的字段。写段代码const StoreContext = createContext(null)export const useStore = () => useContext(StoreContext)const TodoList = observer(() => { const store = useStore() return store.todos.map(todo => <span key={todo.id}>{todo.text}</span>)})
前端阅读 05月30日 01:39

MobX 异步操作为什么要用 runInAction 或 flow?

MobX 处理异步的关键不是“能不能 await”,而是 await 之后的状态修改已经离开原来的 action。开启 enforceActions 时,接口返回后直接改 observable 容易报警,也会让更新边界不清。常用做法有两种:简单请求用 async/await + runInAction,在成功、失败分支里集中更新 data/loading/error;流程复杂、需要取消任务时用 flow(function*(){}),把 await 换成 yield。不要说 async action 会自动包住整个异步过程,它只覆盖同步阶段。追问runInAction 和 flow 怎么选?普通接口请求选 runInAction,写法接近日常 async/await;多步骤流程、需要取消、想少写包装代码时选 flow。为什么 await 后还要重新进 action?因为 await 后是新的 tick,原 action 已结束。MobX 官方也强调 await 后的状态修改不在同一个执行阶段。loading 和 error 应该怎么写?进入请求前设 loading=true、清空 error;成功和失败分支都要把 loading=false 放进 action,避免页面一直转圈。实际项目最常见的坑是什么?最常见是 catch 里只记录错误,忘了重置 loading;其次是连续修改多个字段却没用 runInAction,导致严格模式报警。写段代码async fetchUser(id) { this.loading = true this.error = null try { const data = await api.getUser(id) runInAction(() => { this.user = data; this.loading = false }) } catch (e) { runInAction(() => { this.error = e.message; this.loading = false }) }}
前端阅读 05月27日 23:33

MobX 6 相比 MobX 4/5 有哪些重要变化?

MobX 6 是 MobX 的最新主要版本,与 MobX 4/5 相比有多个破坏性变更和 API 调整。理解这些变化对于项目升级至关重要。核心变化:装饰器默认移除,改用 makeObservableMobX 6 默认不再支持装饰器语法,引入 makeObservable 和 makeAutoObservable 替代。MobX 4/5(装饰器写法):import { observable, action, computed } from 'mobx';class TodoStore { @observable todos = []; @observable filter = 'all'; @computed get completedTodos() { return this.todos.filter(todo => todo.completed); } @action addTodo(text) { this.todos.push({ text, completed: false }); }}MobX 6(推荐写法):import { makeAutoObservable } from 'mobx';class TodoStore { todos = []; filter = 'all'; constructor() { makeAutoObservable(this); } get completedTodos() { return this.todos.filter(todo => todo.completed); } addTodo(text) { this.todos.push({ text, completed: false }); }}makeAutoObservable 自动推断属性类型:getter → computed、方法 → action、其余 → observable。需要精细控制时用 makeObservable,显式标注每个成员:import { makeObservable, observable, action, computed } from 'mobx';class TodoStore { todos = []; filter = 'all'; constructor() { makeObservable(this, { todos: observable, filter: observable, completedTodos: computed, addTodo: action.bound, // 自动绑定 this }); } get completedTodos() { return this.todos.filter(todo => todo.completed); } addTodo(text) { this.todos.push({ text, completed: false }); }}关键区别: makeAutoObservable 不能用于子类(超类和子类都引入 observable 成员时,必须各自调用 makeObservable)。action.bound 只能在 makeObservable 中使用。configure 仍在,默认行为变更原文有误:MobX 6 并未移除 configure API,而是调整了默认值。import { configure } from 'mobx';// MobX 6 的 configure 仍然可用configure({ enforceActions: 'always', // 默认值改为 'observed' computedRequiresReaction: true, // 新增 lint 选项 reactionRequiresObservable: true, // 新增 lint 选项 observableRequiresReaction: true, // 新增 lint 选项 useProxies: 'never', // 可禁用 Proxy});主要变化:enforceActions 默认值从 'never' 改为 'observed',即被观察的状态必须通过 action 修改新增多个 lint 选项帮助捕获常见错误useProxies 可设为 'never' 兼容不支持 Proxy 的环境(如旧版 React Native)Proxy 成为默认机制MobX 6 默认使用 Proxy 实现可观察对象,这意味着:数组和普通对象的属性添加/删除会被自动追踪不再需要 extendObservable 来添加新属性const store = makeAutoObservable({ user: null,});// MobX 5: 新属性不会触发响应// MobX 6: Proxy 自动追踪,以下操作是响应式的store.user = { name: 'Alice' }; // 自动变为 observable如果环境不支持 Proxy,需要配置 useProxies: 'never',此时行为退回 MobX 5 模式,动态添加属性需使用 observable.set() 工具函数。extras 拆分到主 APIextras 命名空间下的工具函数被提升到顶层导出:// MobX 4/5import { extras } from 'mobx';extras.isObservable(obj);extras.getAtom(obs);// MobX 6import { isObservable, getAtom } from 'mobx';isObservable(obj);getAtom(obs);intercept 和 observe 移除intercept 和 observe 在 MobX 6 中被移除,用 reaction / autorun 替代:// MobX 4/5import { observe } from 'mobx';observe(store.todos, (change) => { console.log('Changed:', change);});// MobX 6import { reaction } from 'mobx';reaction( () => [...store.todos], // 追踪整个数组快照 (todos, prevTodos) => { console.log('Todos changed'); });如果需要拦截修改,使用 action 包装修改逻辑。React 集成:弃用 inject/ProviderMobX 6 推荐使用 React Context 替代 mobx-react 的 inject 和 Provider:import { observer } from 'mobx-react-lite';import { createContext, useContext } from 'react';const StoreContext = createContext(null);const useStore = () => { const store = useContext(StoreContext); if (!store) throw new Error('useStore must be within StoreProvider'); return store;};// 函数组件 + observerconst TodoList = observer(() => { const store = useStore(); return <div>{store.completedTodos.length} completed</div>;});// 根组件function App() { return ( <StoreContext.Provider value={todoStore}> <TodoList /> </StoreContext.Provider> );}注意: mobx-react-lite 只支持函数组件。如果项目仍有类组件,继续使用 mobx-react 的 observer HOC,但不再使用 inject。TypeScript 支持改进MobX 6 对 TypeScript 类型推断更完善:class Store { items: Item[] = []; filter: 'all' | 'active' | 'completed' = 'all'; constructor() { // 泛型参数确保类型推断正确 makeAutoObservable<Store>(this, { items: observable.shallow, // 浅层观察,适合数组只关心引用变化 }); } get filteredItems(): Item[] { return this.items.filter(i => i.status === this.filter); }}observable.shallow 是 MobX 6 新增的修饰器,对集合只做浅层响应式转换,避免深层对象都被 proxy 包装,适合存储不可变数据。迁移实战要点1. 装饰器迁移(最关键)每个使用装饰器的类,都需要在 constructor 中添加 makeObservable(this),或改为 makeAutoObservable(this)。可使用官方 mobx-undecorate codemod 自动迁移:npx mobx-undecorate2. 视图不刷新的排查升级后组件不更新,通常是忘记调用 makeObservable(this) 或 makeAutoObservable(this)。MobX 6 要求每个有 observable 成员的类都在构造函数中调用。3. configure 兼容检查项目中所有 configure 调用,确认选项是否需要调整。enforceActions 默认值变为 'observed',可能触发新的警告。4. observable 动态属性MobX 6 使用 Proxy 后,直接赋值新属性会自动变为 observable。但如果禁用了 Proxy,需要用工具函数:import { set, remove } from 'mobx';// 禁用 Proxy 时添加/删除属性set(store, 'newProp', value);remove(store, 'newProp');5. 统一版本MobX 6 合并了 MobX 4(ES5)和 MobX 5(Proxy)两条分支,现在一个包同时支持两种模式,根据 useProxies 配置自动切换。常见追问Q: 能否继续使用装饰器?可以。MobX 6 仍支持旧版装饰器(需 Babel/TS 配置),但将在下个大版本移除。推荐使用 TC39 Stage 3 新装饰器语法 @observable accessor:class Store { @observable accessor count = 0; // 新装饰器语法}Q: makeAutoObservable 和 makeObservable 怎么选?简单 Store 用 makeAutoObservable,代码更简洁。需要 action.bound、observable.shallow、子类继承或排除某些属性时,用 makeObservable 显式标注。Q: 升级后性能会变差吗?不会。Proxy 机制反而比 MobX 5 的 getter/setter 劫持更高效。包体积通过 tree-shaking 也更小。如需极致性能,observable.shallow 可减少深层 proxy 开销。
前端阅读 05月27日 23:31

MobX 的响应式原理是怎样的?依赖收集与更新触发机制详解

MobX 是一个基于透明函数响应式编程(TFRP)的状态管理库,核心思想是:任何源自应用状态的东西都应该自动地获得。它通过 Proxy 拦截对象属性的读写操作,在 getter 中收集依赖、在 setter 中触发更新,实现状态变化后所有依赖方自动响应。响应式原理:依赖收集与触发更新MobX 的核心机制分两个阶段运作:依赖收集阶段——当 autorun、reaction 或 computed 首次执行时,函数内部访问了哪些 observable 属性,MobX 就会记录下这些属性与当前函数的依赖关系。具体实现上,每个 observable 属性内部维护一个 observers 集合,每个 derivation(autorun/computed)内部维护一个 observables 集合,两者互相关联。触发更新阶段——当通过 action 修改 observable 属性时,MobX 遍历该属性的所有 observers,将对应的 derivation 标记为过期并重新执行。import { observable, autorun, action } from 'mobx';const store = observable({ count: 0,});autorun(() => { console.log('count 变化了:', store.count); // 首次执行时收集到 count 依赖});action(() => { store.count++; // 触发 setter → 通知所有 observers → autorun 重新执行})();关键点:autorun 回调在初始化时会同步执行一次,正是这次执行完成了依赖收集。如果回调中没有读取任何 observable 属性,则不会建立任何依赖关系。Observable 的底层实现MobX 6 使用 Proxy 对对象进行深度代理。对于基本类型值,则通过 Atom 类包装:对象/数组:通过 Proxy 的 get 拦截器调用 reportObserved() 记录当前正在执行的 derivation;通过 set 拦截器调用 reportChanged() 通知所有观察者基本类型:通过 observable.box() 包装为带 get/set 方法的盒子对象,内部同样基于 Atom 实现Atom 类:是 MobX 响应式系统的最小单元,提供 reportObserved() 和 reportChanged() 两个核心方法// 简化版 Atom 原理class Atom { observers = new Set(); reportObserved() { if (currentlyTracking) { this.observers.add(currentTrackingDerivation); currentTrackingDerivation.addObservable(this); } } reportChanged() { this.observers.forEach(fn => fn.run()); }}Action 与事务机制Action 不仅仅是"修改状态的方式",它还承担着事务批处理的职责。MobX 在 action 执行前调用 startBatch(),执行后调用 endBatch(),确保一个 action 中多次修改状态只触发一次 derivation 更新。action(() => { store.firstName = 'Zhang'; store.lastName = 'San'; // 不会触发两次 autorun,而是在 endBatch 时统一触发一次})();如果不用 action 直接修改状态,每次赋值都会立即触发更新,可能导致中间状态被响应函数读取,产生不必要的渲染。Computed 的缓存与懒计算Computed 不是简单的"派生值",它有两个重要特性:缓存——只有依赖的 observable 变化时才标记为过期,否则直接返回上次计算的缓存值懒计算——如果没有 observer 消费这个 computed,它永远不会执行计算逻辑内部实现上,computed 同时是 derivation(依赖 observable)和 observable(被其他 derivation 观察),处于依赖链的中间层。MobX 与 Redux 的核心差异| 维度 | MobX | Redux ||------|------|-------|| 更新方式 | 可变状态,直接赋值 | 不可变状态,返回新对象 || 订阅机制 | 自动依赖追踪 | 手动 connect/subscribe || 样板代码 | 极少 | 较多(action type、reducer、dispatch) || 状态结构 | 支持嵌套对象图 | 推荐扁平化 normalized 结构 || 时间旅行 | 不原生支持 | 天然支持 || 更新粒度 | 属性级别精确更新 | 组件级别浅比较 |MobX 适合状态结构复杂、嵌套深、追求开发效率的场景;Redux 适合需要严格数据流、时间旅行调试、团队规模大的项目。面试追问方向MobX 如何处理异步 action? 需要用 runInAction 包裹异步回调中的状态修改,或者使用 flow + generator 函数为什么 MobX 不建议在 autorun 中做异步操作? 异步回调中的 observable 读取不会被追踪,因为依赖收集是同步完成的makeAutoObservable 和 makeObservable 的区别? 前者自动推断成员类型,后者需要显式标注,后者更适合需要精确控制的场景
前端阅读 05月27日 23:31

MobX 和 Redux 有什么区别?

MobX 和 Redux 有什么区别?面试中三句话说清楚:MobX 是响应式自动追踪,改了数据视图自动更新;Redux 是函数式单向数据流,必须 dispatch action 才能改状态。MobX 写得少但调试难预测,Redux 写得多但状态可追溯。选哪个看团队——要快用 MobX,要严用 Redux。核心区别| 维度 | MobX | Redux ||------|------|-------|| 编程范式 | 响应式 + 面向对象 | 函数式 + 单向数据流 || 状态修改 | 直接赋值,自动追踪 | dispatch action → reducer 返回新状态 || 样板代码 | 极少 | 较多(即使 RTK 也比 MobX 多) || 状态结构 | 嵌套对象随意写 | 推荐扁平化 + normalize || 时间旅行 | 有限支持 | Redux DevTools 完整支持 || 学习曲线 | 入门快,精通需理解响应式原理 | 入门慢,但模式固定好掌握 || TypeScript | 良好 | 良好(RTK 出厂即支持) |代码对比:同一个 TodoMobX 写法import { makeAutoObservable, computed } from "mobx";class TodoStore { todos = []; filter = "all"; constructor() { makeAutoObservable(this); } get filteredTodos() { if (this.filter === "completed") return this.todos.filter((t) => t.done); if (this.filter === "active") return this.todos.filter((t) => !t.done); return this.todos; } addTodo(text) { this.todos.push({ id: Date.now(), text, done: false }); } toggle(id) { const todo = this.todos.find((t) => t.id === id); if (todo) todo.done = !todo.done; }}直接改属性,MobX 内部的依赖追踪机制会自动触发对应组件重渲染。这就是响应式的核心——你写的是普通赋值,背后 MobX 帮你做了订阅和通知。Redux Toolkit 写法2026 年 Redux 官方推荐用 Redux Toolkit(RTK),不再用 createStore 那套手写模板。import { createSlice, configureStore, createSelector } from "@reduxjs/toolkit";const todoSlice = createSlice({ name: "todos", initialState: { items: [], filter: "all" }, reducers: { addTodo: (state, action) => { state.items.push({ id: Date.now(), text: action.payload, done: false }); }, toggle: (state, action) => { const todo = state.items.find((t) => t.id === action.payload); if (todo) todo.done = !todo.done; }, setFilter: (state, action) => { state.filter = action.payload; }, },});export const { addTodo, toggle, setFilter } = todoSlice.actions;const store = configureStore({ reducer: { todos: todoSlice.reducer } });// Selector(带 memo)const selectFiltered = createSelector( [(s) => s.todos.items, (s) => s.todos.filter], (items, filter) => { if (filter === "completed") return items.filter((t) => t.done); if (filter === "active") return items.filter((t) => !t.done); return items; });RTK 内置了 Immer,所以在 reducer 里可以直接修改state(实际产出的是不可变新对象)。这大大减少了 Redux 的样板代码量。面试追问:MobX 的响应式原理是什么?MobX 在属性读取时收集依赖(通过 Proxy 或 getter 劫持),在属性写入时通知所有观察者。组件渲染时读取 observable 属性,MobX 记录这个组件依赖这些属性;属性变化时,MobX 精确触发对应组件重渲染。所以 MobX 不需要手动 shouldComponentUpdate 或 React.memo,它天然做到了最小化更新。代价是调试时不容易追踪谁改了这个值,因为赋值点分散在代码各处。面试追问:为什么 Redux 要求状态不可变?两个原因。第一,不可变让引用比较成为可能——oldState !== newState 就知道状态变了,不用深比较,这是 Redux 性能模型的基础。第二,不可变保证了时间旅行调试——每次状态变更都产生新的快照,可以回退到任意历史节点。如果直接修改原对象,历史状态会被覆盖,DevTools 的时间旅行就废了。这也是 MobX 时间旅行支持有限的根本原因。性能:谁更快?2026 年基准测试数据:| 操作 | MobX | Redux Toolkit ||------|------|---------------|| 简单更新 | 0.3ms | 0.8ms || 嵌套更新 | 0.4ms | 1.2ms || 内存占用 | 3.1MB | 4.2MB |MobX 快在哪?它自动追踪依赖,只更新真正受影响的组件。Redux 每次 dispatch 后要过一遍 useSelector 的比较逻辑,组件需要自己决定要不要重渲染。当然,Redux 配合 reselect 做 memo 化后差距会缩小,但这是需要开发者手动做的。怎么选?选 MobX: 小团队快速迭代、状态嵌套深(比如树形编辑器)、团队 OOP 背景强、不想写样板代码。选 Redux (RTK): 大型项目多人协作、需要严格的代码规范和可追溯的状态变更、需要 DevTools 时间旅行、团队函数式偏好。都不选? 2026 年 Zustand(2.1KB)因为极简 API 和零样板代码,成为很多新项目的默认选择。它没有 MobX 的响应式黑盒,也没有 Redux 的模板负担。如果你的项目状态管理不复杂,Zustand 值得一看。一句话总结MobX 用魔法帮你省事,Redux 用规矩帮你兜底。面试答区别,先说范式(响应式 vs 函数式),再说可变性(可变 vs 不可变),最后说取舍(灵活 vs 可预测)。
前端阅读 05月27日 23:25

MobX 中 action 的作用和使用方法是什么?

核心答案action 是 MobX 中修改 observable 状态的推荐方式。它将状态变更包裹在事务中,确保内部的多次修改只触发一次 reaction,同时让状态变更可追踪、可调试。关键点:action 内的状态变更会批量处理,action 结束后才通知观察者严格模式下(enforceActions: 'always'),所有状态变更必须通过 action 完成只对修改状态的函数使用 action,纯查询/计算用 computedaction 的三种声明方式makeAutoObservable(推荐)class TodoStore { todos = []; constructor() { makeAutoObservable(this); } addTodo(text) { this.todos.push({ text, completed: false }); } removeTodo(id) { this.todos = this.todos.filter(t => t.id !== id); }}makeAutoObservable 会自动推断:有参数的方法标记为 action,getter 标记为 computed,其余为 observable。makeObservable(需显式标注)class TodoStore { todos = []; constructor() { makeObservable(this, { todos: observable, addTodo: action, removeTodo: action.bound, }); } addTodo(text) { this.todos.push({ text, completed: false }); } removeTodo(id) { this.todos = this.todos.filter(t => t.id !== id); }}action.bound 解决 this 丢失action.bound 自动绑定 this 到实例,传给回调时不会丢失上下文:class Store { count = 0; constructor() { makeAutoObservable(this); } increment = action.bound(() => { this.count++; });}const store = new Store();document.addEventListener('click', store.increment); // this 正确异步 action 的正确写法async 函数中,await 之后的代码已经脱离了 action 上下文,必须用 runInAction 或 flow 包裹。runInActionasync fetchTodos() { this.loading = true; try { const res = await fetch('/api/todos'); const data = await res.json(); runInAction(() => { this.todos = data; this.loading = false; }); } catch (e) { runInAction(() => { this.error = e.message; this.loading = false; }); }}flow(推荐,更简洁)fetchTodos = flow(function* () { this.loading = true; try { const res = yield fetch('/api/todos'); const data = yield res.json(); this.todos = data; this.loading = false; } catch (e) { this.error = e.message; this.loading = false; }});flow 用 generator 替代 async/await,每个 yield 之后自动回到 action 上下文,无需手动 runInAction。enforceActions 配置在 configure 中开启严格模式,强制所有状态变更走 action:import { configure } from 'mobx';configure({ enforceActions: 'always' });// 'observed' — 仅在观察者存在时强制// 'always' — 始终强制,最严格// 'never' — 不强制(默认)大型项目建议设为 'always',避免随意修改状态导致难以排查的 bug。常见坑1. async 函数中 await 后直接改状态 — 状态变更不在 action 中,严格模式下报错。用 runInAction 或 flow。2. action.bound 和箭头函数混用 — 箭头函数本身就是绑定过的,再套 action.bound 无意义:// 错误:箭头函数不能重新绑定increment = action.bound(() => { this.count++; });// 正确:用普通方法increment() { this.count++; }// 然后在 makeObservable 中标记为 action.bound3. 在 action 中做纯计算 — 查询、过滤等不修改状态的逻辑不应标记为 action,否则 MobX 无法追踪其依赖,应使用 computed。
服务端阅读 05月27日 18:30

MobX 中的中间件和拦截器如何使用?

MobX 生态中有两套不同的拦截与中间件机制:MobX 核心库的 intercept/observe,以及 MobX-State-Tree(MST)的 addMiddleware/onAction。面试中混淆两者是常见的扣分点。下面分别讲解它们的用法、区别和典型场景。核心库:intercept 和 observeintercept 和 observe 是 MobX 核心库提供的底层 API,直接作用于 observable 对象的属性变更。intercept:变更前拦截intercept(target, propertyName?, interceptor) 在变更作用于 observable 之前被调用,可以对变更进行修改、放行或取消。import { observable, intercept } from 'mobx';const store = observable({ count: 0, items: []});// 拦截 count 属性的变化const disposer = intercept(store, 'count', (change) => { // 1. 修改变更:不允许负数 if (change.newValue < 0) { change.newValue = 0; } // 2. 取消变更:超过上限直接返回 null if (change.newValue > 100) { return null; } // 3. 放行变更:返回 change 对象 return change;});store.count = 5; // 正常设置,count 变为 5store.count = -1; // 被修改,count 变为 0store.count = 200; // 被取消,count 保持不变disposer(); // 移除拦截器拦截器的返回值决定了变更的命运:返回 change 对象:放行变更修改 change 后返回:修改后放行(常用于数据规范化)返回 null:取消变更,对象不被修改抛出异常:阻止变更并向上传播错误拦截数组和 Mapintercept 也可以作用于 observable 数组和 Map,此时不需要指定属性名:import { observable, intercept } from 'mobx';const items = observable([1, 2, 3]);intercept(items, (change) => { if (change.type === 'add' && typeof change.newValue !== 'number') { throw new Error('只允许添加数字'); } return change;});const map = observable(new Map());intercept(map, (change) => { if (change.name === 'secret') { return null; // 禁止设置 secret 键 } return change;});observe:变更后观察observe(target, propertyName?, listener) 在变更已经生效之后被调用,适合做副作用处理(如日志、同步到外部系统)。import { observable, observe } from 'mobx';const store = observable({ count: 0 });const disposer = observe(store, 'count', (change) => { console.log(`count: ${change.oldValue} -> ${change.newValue}`);});store.count = 5; // 输出: count: 0 -> 5disposer();观察数组时的 change 对象包含 added、removed、index 等字段:const items = observable([1, 2, 3]);observe(items, (change) => { if (change.type === 'splice') { console.log('添加:', change.added, '移除:', change.removed); }});items.push(4); // 添加: [4] 移除: []不指定属性名时,可以观察对象所有属性的变化:observe(store, (change) => { console.log(`${change.name}: ${change.oldValue} -> ${change.newValue}`);});intercept 与 observe 的关键区别| 对比项 | intercept | observe ||--------|-----------|---------|| 触发时机 | 变更生效前 | 变更生效后 || 能否修改变更 | 可以 | 不可以 || 能否取消变更 | 可以(返回 null) | 不可以 || 典型用途 | 数据验证、格式化、权限控制 | 日志记录、副作用同步 |注意事项MobX 官方文档明确指出,intercept 和 observe 是底层工具,在实际项目中应谨慎使用。原因如下:observe 不遵循事务原则,在 action 中间可能触发多次两者都不支持深层级对象变化的监听滥用 intercept 容易创建难以调试的隐式数据流优先使用 reaction、autorun 或 when 来替代 observe;将数据验证逻辑放在 action 内部而不是 intercept 中。MobX-State-Tree:中间件体系MobX-State-Tree(MST)在 MobX 核心之上构建了更完善的中间件系统,通过 addMiddleware 和 onAction 提供 action 级别的拦截能力。addMiddleware:拦截 actionaddMiddleware 可以拦截子树上的任何 action 调用,并能修改参数、中止执行或替换返回值。import { addMiddleware, flow } from 'mobx-state-tree';const disposer = addMiddleware(store, (call, next) => { // call 包含: name, args, type, context, tree 等 console.log(`[Action] ${call.name} 被调用,参数:`, call.args); // 前置逻辑:验证参数 if (call.name === 'removeItem' && call.args[0] < 0) { return next({ ...call, args: [0] }); // 修改参数后传递 } // 调用 next 继续执行链 const result = next(call); // 后置逻辑:记录结果 console.log(`[Action] ${call.name} 完成,结果:`, result); return result;});中间件处理函数必须调用 next(call) 让 action 继续执行,或者通过返回值中止 action。不调用 next 会导致 action 被静默取消。onAction:监听 actiononAction 是一个内置的只读中间件,只能监听 action 的调用,不能拦截或修改。import { onAction } from 'mobx-state-tree';const disposer = onAction(store, (call) => { console.log(`Action ${call.name} 被调用,参数:`, call.args);});onAction 的参数以可序列化格式传递,适合用于:调试日志操作录制与重放(配合 applyAction)远程同步onAction 与 addMiddleware 的区别| 对比项 | addMiddleware | onAction ||--------|---------------|----------|| 能否拦截 action | 可以 | 不可以 || 能否修改参数 | 可以(克隆后修改) | 不可以 || 能否中止执行 | 可以(不调用 next) | 不可以 || 参数格式 | 原始参数 | 可序列化格式 || 典型用途 | 验证、权限控制、错误处理 | 日志、录制、调试 |中间件链的执行顺序多个中间件可以附加到同一个节点上,执行顺序遵循"由内到外"原则:同一对象上,先注册的中间件先执行子节点的中间件先于父节点的中间件执行每个中间件必须调用 next(call) 才能将控制权传递给下一个实战:典型应用场景数据验证与格式化用 intercept 在数据写入前进行校验和规范化:import { observable, intercept } from 'mobx';const form = observable({ email: '', age: 0});intercept(form, 'email', (change) => { if (change.newValue && !change.newValue.includes('@')) { return null; // 不写入无效邮箱 } return change;});intercept(form, 'age', (change) => { change.newValue = Math.max(0, Math.floor(change.newValue)); return change;});撤销/重做(Undo/Redo)利用 observe 记录变更历史,实现撤销和重做功能:import { observable, observe, action, makeAutoObservable } from 'mobx';class UndoManager { past = []; future = []; constructor(target) { this.target = target; makeAutoObservable(this); // 监听目标对象的属性变化 Object.keys(target).forEach((key) => { observe(target, key, (change) => { this.past.push({ key, oldValue: change.oldValue, newValue: change.newValue }); this.future = []; }); }); } @action undo() { if (this.past.length === 0) return; const entry = this.past.pop(); this.future.push(entry); this.target[entry.key] = entry.oldValue; } @action redo() { if (this.future.length === 0) return; const entry = this.future.pop(); this.past.push(entry); this.target[entry.key] = entry.newValue; } get canUndo() { return this.past.length > 0; } get canRedo() { return this.future.length > 0; }}MST 中间件:统一的错误处理在 MST 中用 addMiddleware 为所有 action 统一添加错误处理:import { addMiddleware } from 'mobx-state-tree';addMiddleware(store, (call, next) => { try { const result = next(call); // 异步 action 需要特殊处理 if (result && typeof result.then === 'function') { return result.catch((error) => { console.error(`[Error] ${call.name} 失败:`, error); store.setError(error.message); throw error; }); } return result; } catch (error) { console.error(`[Error] ${call.name} 失败:`, error); store.setError(error.message); throw error; }});MST 中间件:性能监控import { addMiddleware } from 'mobx-state-tree';const metrics = {};addMiddleware(store, (call, next) => { const start = performance.now(); const result = next(call); const duration = performance.now() - start; if (!metrics[call.name]) { metrics[call.name] = { count: 0, totalTime: 0, maxTime: 0 }; } const m = metrics[call.name]; m.count++; m.totalTime += duration; m.maxTime = Math.max(m.maxTime, duration); if (duration > 100) { console.warn(`[性能] ${call.name} 耗时 ${duration.toFixed(2)}ms`); } return result;});操作录制与重放利用 onAction 的可序列化特性,录制操作并在其他实例上重放:import { onAction, applyAction } from 'mobx-state-tree';// 录制端const recordedActions = [];onAction(sourceStore, (call) => { recordedActions.push(call);});// 重放端recordedActions.forEach((action) => { applyAction(targetStore, action);});这种模式在协作编辑、时间旅行调试和测试中非常有用。面试高频问题intercept 和 observe 的区别是什么?intercept 在变更生效前触发,可以修改或取消变更;observe 在变更生效后触发,只能被动接收。前者适合数据校验和格式化,后者适合日志和副作用同步。为什么 MobX 官方建议慎用 intercept 和 observe?因为 observe 不遵循事务原则,可能在一个 action 中间多次触发;两者都不支持深层级监听;滥用容易创建隐式的、难以调试的数据流。官方推荐使用 reaction、autorun 或 when 替代。MST 的 addMiddleware 和 onAction 有什么区别?addMiddleware 可以拦截、修改和中止 action,而 onAction 只能监听不能拦截。onAction 的参数以可序列化格式传递,适合录制和重放场景。MST 中间件链的执行顺序是什么?同一对象上先注册的中间件先执行,子节点中间件先于父节点中间件执行。每个中间件必须调用 next(call) 才能将控制权传递给下一个。如何选择使用哪种机制?需要拦截 observable 属性级别的变更:用核心库 intercept需要监听属性变更做副作用:优先用 reaction,其次 observe需要拦截 MST action 级别的调用:用 addMiddleware只需监听 MST action 调用:用 onAction需要数据验证:放在 action 逻辑内部,而非 intercept 中
前端阅读 05月27日 18:16

MobX 性能优化的最佳实践有哪些?

MobX 本身已经做了大量性能优化——细粒度依赖追踪、自动批处理、computed 缓存,大多数场景下开箱即用就够了。真正需要手动优化的,集中在三件事上:computed 被滥用或用错了、observable 追踪了不该追踪的东西、组件粒度太粗导致重渲染范围过大。核心思路:减少追踪范围(只让真正会变的状态变 observable)、减少计算次数(用 computed 缓存派生值)、减少渲染范围(拆小组件、延迟间接引用)。追问computed 和普通 getter 有什么区别?什么时候该用 computed?computed 会缓存结果,只在依赖的 observable 变化时重新计算;普通 getter 每次访问都执行。当你需要从 observable 数据派生新值时用 computed——过滤列表、拼接字符串、计算汇总。一个值如果会被多处读取,computed 的缓存收益更大。注意:computed 里不能有副作用(发请求、改状态),它必须是纯函数,否则缓存一致性无法保证。observable 的深度怎么选?什么时候用 shallow?默认 observable 会递归把对象所有层级都变成响应式,适合嵌套深、内部属性需要单独追踪的场景。observable.shallowObject 只让第一层变成响应式,内部对象保持原样。实际项目中,列表数据用 shallow 就够了——你通常关心的是"列表变了"而不是"某个用户的名字变了"。只有确实需要追踪深层属性变化时才用深度 observable。对于不会变的配置项(API 地址、超时时间),压根不要加 observable,纯常量没必要追踪。action 里还需要包 runInAction 吗?不需要。action 本身就会批量处理里面的状态变更,在 action 内再套 runInAction 是多余的。runInAction 的真正用途是异步回调中修改状态——await 之后的赋值已经不在 action 作用域内,必须用 runInAction 包起来。@actionasync fetchData() { this.loading = true; const data = await api.getData(); // 这里已经不在 action 作用域了 runInAction(() => { this.data = data; this.loading = false; });}observer 组件拆多细合适?看组件里读了几种不同的 observable。一个组件同时读 user.name、settings.theme、data.list,任何一个变化都会触发整个组件重渲染。拆成三个小组件,各自只读自己关心的数据,交叉触发就消失了。判断标准:observable 依赖越集中越好。一块 UI 只依赖 store 的一小块数据,就值得单独抽成 observer 组件。如果整个页面只读一个 observable,拆不拆无所谓。另外,不读 observable 的组件(纯展示的 Header、Footer)不要加 observer,加了反而增加追踪开销。autorun、reaction、when 怎么选?autorun:立即执行一次,之后依赖变化就重新执行。适合日志、同步等"每次变了都要做某事"的场景。reaction:只追踪数据表达式,数据变了才执行副作用回调,默认不立即执行。比 autorun 更可控,优先用 reaction。when:条件满足时执行一次就自动销毁。适合"等数据到了再做某事"的一次性逻辑,比在 autorun 里写 if 判断更清晰。三者的返回值都是 dispose 函数,组件卸载时一定要调用,否则内存泄漏。数组操作有什么性能坑?避免整体重新赋值(this.items = [...this.items, item]),MobX 会对整个数组重新建立追踪。用 push、splice 等变异方法直接操作,MobX 只追踪变化的部分。批量替换用 replace(newArray),比重新赋值高效,MobX 内部会做差异更新而不是重建整个 observable 结构。怎么排查 MobX 的性能问题?用 trace() 定位是哪个 computed 或 reaction 导致了多余计算。在组件 render 里调用 trace(true),控制台会输出完整的依赖链和触发原因。用 MobX DevTools 观察每次状态变更触发了哪些 reaction,找到重渲染次数异常的组件。如果某个 computed 计算太频繁,检查它的依赖范围是不是比预期的大——可能是间接引用了一个大对象,MobX 会追踪这个对象上所有被读取的属性。用 computed 预处理数据,把 map/filter 的结果缓存起来,避免在 observer 组件的 render 里直接遍历大列表。写段代码// makeAutoObservable 一键搞定 observable/computed/action 标记class Store { items = []; filter = 'all'; constructor() { makeAutoObservable(this); } get filteredItems() { return this.filter === 'all' ? this.items : this.items.filter(i => i.active); } setFilter(f) { this.filter = f; }}