面试题手册

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

前端阅读 02026年5月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) }}
前端阅读 02026年5月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 }}
前端阅读 02026年5月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()
服务端阅读 02026年5月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>)})
前端阅读 02026年5月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 中