标签

Mobx

MobX是一个基于信号的、经过实战测试的库,通过透明地应用函数式响应式编程,使状态管理变得简单和可扩展。

Mobx
查看更多相关内容
前端2026年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 对象直接传给不支持代理的第三方库;三是冻结对象或随意替换深层结构,导致追踪和更新不符合预期。 ## 写段代码 ```javascript 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) } } ```
前端2026年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,或使用结构比较配置。 ## 写段代码 ```javascript class TodoStore { todos = [] filter = 'all' constructor() { makeAutoObservable(this) } get visibleTodos() { if (this.filter === 'done') return this.todos.filter(t => t.done) return this.todos } } ```
前端2026年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 回答“变化后做什么”。 ## 写段代码 ```javascript const dispose = reaction( () => store.query, query => { if (query.length > 2) store.search(query) }, { delay: 300 } ) // React 卸载或不再需要时 // dispose() ```
服务端2026年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,传入前最好转成普通数据,或只传它需要的字段。 ## 写段代码 ```jsx 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>) }) ```
前端2026年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`,导致严格模式报警。 ## 写段代码 ```javascript 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 }) } } ```
前端5月27日 23:33
MobX 6 相比 MobX 4/5 有哪些重要变化?MobX 6 是 MobX 的最新主要版本,与 MobX 4/5 相比有多个破坏性变更和 API 调整。理解这些变化对于项目升级至关重要。 ## 核心变化:装饰器默认移除,改用 makeObservable MobX 6 默认不再支持装饰器语法,引入 `makeObservable` 和 `makeAutoObservable` 替代。 **MobX 4/5(装饰器写法):** ```javascript 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(推荐写法):** ```javascript 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`,显式标注每个成员: ```javascript 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,而是调整了默认值。 ```javascript 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` 来添加新属性 ```javascript const store = makeAutoObservable({ user: null, }); // MobX 5: 新属性不会触发响应 // MobX 6: Proxy 自动追踪,以下操作是响应式的 store.user = { name: 'Alice' }; // 自动变为 observable ``` 如果环境不支持 Proxy,需要配置 `useProxies: 'never'`,此时行为退回 MobX 5 模式,动态添加属性需使用 `observable.set()` 工具函数。 ## extras 拆分到主 API `extras` 命名空间下的工具函数被提升到顶层导出: ```javascript // MobX 4/5 import { extras } from 'mobx'; extras.isObservable(obj); extras.getAtom(obs); // MobX 6 import { isObservable, getAtom } from 'mobx'; isObservable(obj); getAtom(obs); ``` ## intercept 和 observe 移除 `intercept` 和 `observe` 在 MobX 6 中被移除,用 `reaction` / `autorun` 替代: ```javascript // MobX 4/5 import { observe } from 'mobx'; observe(store.todos, (change) => { console.log('Changed:', change); }); // MobX 6 import { reaction } from 'mobx'; reaction( () => [...store.todos], // 追踪整个数组快照 (todos, prevTodos) => { console.log('Todos changed'); } ); ``` 如果需要拦截修改,使用 `action` 包装修改逻辑。 ## React 集成:弃用 inject/Provider MobX 6 推荐使用 React Context 替代 `mobx-react` 的 `inject` 和 `Provider`: ```javascript 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; }; // 函数组件 + observer const 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 类型推断更完善: ```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 自动迁移: ```bash npx mobx-undecorate ``` ### 2. 视图不刷新的排查 升级后组件不更新,通常是忘记调用 `makeObservable(this)` 或 `makeAutoObservable(this)`。MobX 6 要求**每个有 observable 成员的类**都在构造函数中调用。 ### 3. configure 兼容 检查项目中所有 `configure` 调用,确认选项是否需要调整。`enforceActions` 默认值变为 `'observed'`,可能触发新的警告。 ### 4. observable 动态属性 MobX 6 使用 Proxy 后,直接赋值新属性会自动变为 observable。但如果禁用了 Proxy,需要用工具函数: ```javascript 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`: ```javascript 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 开销。
前端5月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 标记为过期并重新执行。 ```javascript 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()` 两个核心方法 ```javascript // 简化版 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 更新。 ```javascript action(() => { store.firstName = 'Zhang'; store.lastName = 'San'; // 不会触发两次 autorun,而是在 endBatch 时统一触发一次 })(); ``` 如果不用 action 直接修改状态,每次赋值都会立即触发更新,可能导致中间状态被响应函数读取,产生不必要的渲染。 ## Computed 的缓存与懒计算 Computed 不是简单的"派生值",它有两个重要特性: 1. **缓存**——只有依赖的 observable 变化时才标记为过期,否则直接返回上次计算的缓存值 2. **懒计算**——如果没有 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 的区别?** 前者自动推断成员类型,后者需要显式标注,后者更适合需要精确控制的场景
前端5月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 出厂即支持) | ## 代码对比:同一个 Todo ### MobX 写法 ```javascript 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` 那套手写模板。 ```javascript 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 可预测)。
前端5月27日 23:25
MobX 中 action 的作用和使用方法是什么?## 核心答案 action 是 MobX 中修改 observable 状态的推荐方式。它将状态变更包裹在事务中,确保内部的多次修改只触发一次 reaction,同时让状态变更可追踪、可调试。 关键点: - action 内的状态变更会批量处理,`action` 结束后才通知观察者 - 严格模式下(`enforceActions: 'always'`),所有状态变更必须通过 action 完成 - 只对**修改**状态的函数使用 action,纯查询/计算用 computed ## action 的三种声明方式 ### makeAutoObservable(推荐) ```javascript 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(需显式标注) ```javascript 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 到实例,传给回调时不会丢失上下文: ```javascript 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` 包裹。 ### runInAction ```javascript async 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(推荐,更简洁) ```javascript 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: ```javascript import { configure } from 'mobx'; configure({ enforceActions: 'always' }); // 'observed' — 仅在观察者存在时强制 // 'always' — 始终强制,最严格 // 'never' — 不强制(默认) ``` 大型项目建议设为 `'always'`,避免随意修改状态导致难以排查的 bug。 ## 常见坑 **1. async 函数中 await 后直接改状态** — 状态变更不在 action 中,严格模式下报错。用 `runInAction` 或 `flow`。 **2. action.bound 和箭头函数混用** — 箭头函数本身就是绑定过的,再套 `action.bound` 无意义: ```javascript // 错误:箭头函数不能重新绑定 increment = action.bound(() => { this.count++; }); // 正确:用普通方法 increment() { this.count++; } // 然后在 makeObservable 中标记为 action.bound ``` **3. 在 action 中做纯计算** — 查询、过滤等不修改状态的逻辑不应标记为 action,否则 MobX 无法追踪其依赖,应使用 computed。
服务端5月27日 18:30
MobX 中的中间件和拦截器如何使用?MobX 生态中有两套不同的拦截与中间件机制:MobX 核心库的 `intercept`/`observe`,以及 MobX-State-Tree(MST)的 `addMiddleware`/`onAction`。面试中混淆两者是常见的扣分点。下面分别讲解它们的用法、区别和典型场景。 ## 核心库:intercept 和 observe `intercept` 和 `observe` 是 MobX 核心库提供的底层 API,直接作用于 observable 对象的属性变更。 ### intercept:变更前拦截 `intercept(target, propertyName?, interceptor)` 在变更作用于 observable 之前被调用,可以对变更进行修改、放行或取消。 ```javascript 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 变为 5 store.count = -1; // 被修改,count 变为 0 store.count = 200; // 被取消,count 保持不变 disposer(); // 移除拦截器 ``` 拦截器的返回值决定了变更的命运: - 返回 `change` 对象:放行变更 - 修改 `change` 后返回:修改后放行(常用于数据规范化) - 返回 `null`:取消变更,对象不被修改 - 抛出异常:阻止变更并向上传播错误 ### 拦截数组和 Map `intercept` 也可以作用于 observable 数组和 Map,此时不需要指定属性名: ```javascript 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)` 在变更已经生效之后被调用,适合做副作用处理(如日志、同步到外部系统)。 ```javascript 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 -> 5 disposer(); ``` 观察数组时的 change 对象包含 `added`、`removed`、`index` 等字段: ```javascript const items = observable([1, 2, 3]); observe(items, (change) => { if (change.type === 'splice') { console.log('添加:', change.added, '移除:', change.removed); } }); items.push(4); // 添加: [4] 移除: [] ``` 不指定属性名时,可以观察对象所有属性的变化: ```javascript 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:拦截 action `addMiddleware` 可以拦截子树上的任何 action 调用,并能修改参数、中止执行或替换返回值。 ```javascript 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:监听 action `onAction` 是一个内置的只读中间件,只能监听 action 的调用,不能拦截或修改。 ```javascript 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` 在数据写入前进行校验和规范化: ```javascript 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` 记录变更历史,实现撤销和重做功能: ```javascript 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 统一添加错误处理: ```javascript 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 中间件:性能监控 ```javascript 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` 的可序列化特性,录制操作并在其他实例上重放: ```javascript 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` 中