前端5月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` 在条件满足后执行一次就销毁。
```ts
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` 变化时,依赖链被标记为过期,组件才重新渲染。
```tsx
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。标签
Mobx
MobX是一个基于信号的、经过实战测试的库,通过透明地应用函数式响应式编程,使状态管理变得简单和可扩展。

前端5月31日 15:55
MobX 和 Redux 到底该怎么选?适合哪些场景?MobX 和 Redux 的区别不只是 API 写法不同,而是状态管理哲学不同。Redux 强调显式数据流:组件 dispatch action,reducer 生成新 state,状态变化可以被记录和回放。MobX 强调响应式模型:你修改 observable,系统自动知道哪些 computed、reaction 或 observer 组件需要更新。
如果用一句话选型:需要强约束、审计和统一协作时偏 Redux;需要快速建模复杂业务对象、减少样板代码时偏 MobX。现在 Redux Toolkit 已经大幅减少模板代码,所以不能再简单说“Redux 一定啰嗦”。但 MobX 在深层对象、表单状态和局部复杂交互里仍然很顺手。
```ts
// Redux Toolkit
const slice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
inc: state => { state.value += 1; }
}
});
// MobX
class 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 的效率。服务端5月31日 15:55
MobX 中 observable、computed 和 action 该怎么分工?`observable`、`computed` 和 `action` 是 MobX 里最容易混在一起的三个词,但分工其实很清楚:`observable` 放状态,`computed` 放由状态推导出的值,`action` 放修改状态的过程。一个常见判断是,如果它需要被 UI 观察,就用 observable;如果它能由已有状态算出来,就别再存一份;如果它会改变状态,就让它进入 action 边界。
```ts
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,页面就会一直转圈。
```ts
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 用错,而是把可观察状态、计算逻辑和副作用混在同一段代码里。前端5月31日 15:55
MobX 依赖追踪到底是怎么知道该更新谁的?MobX 依赖追踪的核心可以用一句话概括:谁在运行时读了 observable,谁就会被登记为它的依赖;以后这个 observable 变了,只通知登记过的人。这里的“谁”可能是 `autorun`、`reaction`、`computed`,也可能是被 `observer` 包装的 React 组件。MobX 不靠你手写依赖数组,而是靠运行时读取行为建立依赖图。
一个最小例子如下。`autorun` 第一次执行时会读取 `store.count`,MobX 会把当前 reaction 和 `count` 这个可观察字段连起来。之后 `count` 改变,`autorun` 会重新执行;但如果改的是 `name`,这个 reaction 不会受影响。
```ts
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++; // 触发 autorun
store.name = "Redux"; // 不触发上面的 autorun
dispose();
```
从内部看,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 起清楚的名字,比事后猜哪个字段触发更新要省很多时间。
```ts
autorun(() => {
trace();
console.log(store.visibleUserName);
});
```
MobX 的依赖追踪并不神秘,它只是把“读”和“写”都接管了。理解这一点后,很多问题都能归结为两个检查:状态有没有被观察,读取有没有发生在 reaction 执行期间。服务端5月31日 15:55
React 里 MobX observer 为什么能自动更新组件?MobX 的 `observer` 不是简单地给组件加一个订阅开关,它会在组件渲染时记录“这次 render 到底读了哪些 observable”。之后只有这些被读过的状态变化,组件才会重新渲染。也就是说,`observer` 的关键不是“组件用了 store”,而是“组件在渲染期间访问了 store 的哪个字段”。
在 React 项目里,函数组件通常使用 `mobx-react-lite`,类组件才会用到 `mobx-react`。MobX 6 以后更推荐 `makeAutoObservable`,少写装饰器,也更适合 TypeScript 和现代构建环境。
```tsx
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 还能合并通知,减少组件中间态闪烁。
```ts
async load() {
this.loading = true;
const data = await api.getList();
runInAction(() => {
this.items = data;
this.loading = false;
});
}
```
`observer` 用得好,核心不是“所有组件都包一下”,而是让组件在正确的位置读取正确的 observable。状态读取越靠近展示位置,MobX 的自动追踪越准确,页面也越不容易出现莫名其妙的刷新。服务端5月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` 里。这样状态变更集中,组件只看到清晰的开始、成功或失败、结束。
```javascript
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 类型也要多处理一点,团队不熟时会增加理解成本。
```javascript
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 闪烁。
```javascript
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 安全,而是为了避免无意义请求和过期数据写回。服务端5月31日 15:55
大型前端应用中 MobX Store 应该如何拆分和协作?大型应用里用 MobX,关键不是“建一个 store 然后全局导出”,而是把状态边界划清楚。页面越多,store 越容易变成杂物间:登录信息、列表缓存、弹窗状态、错误提示、持久化逻辑全塞一起,短期写得快,后期任何改动都像拆炸弹。比较稳的做法是用 RootStore 管组合关系,业务 Store 管自己的数据和动作,跨模块协作只通过少数明确入口发生。
## 推荐的基本结构
RootStore 不应该写太多业务逻辑,它更像装配层。UserStore、ProductStore、CartStore 等模块各自维护领域状态,构造时拿到 root 或必要依赖。也方便测试时替换依赖。
```typescript
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 只做必要派生。边界是:它应该没有副作用,也不应该偷偷发请求或修改状态。
```javascript
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 才能跑,通常说明业务逻辑拆分还不够清楚。前端5月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`。三者不是替代关系。
```javascript
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' }`。它的好处是减少深层追踪开销,代价是必须使用不可变更新风格。团队里如果有人习惯直接改嵌套字段,就很容易出现数据变了但页面没动。
```javascript
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 里显式赋值。这个边界在表单草稿里很有用:先复制一份编辑,保存时再提交。踩坑点是把快照传来传去后误以为还在响应式系统内,结果修改没有任何观察者收到通知。前端5月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 数据和真实接口结构不一致。
```ts
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 类型,最后覆盖率上去了,坏业务却没被测到。取舍上,调试工具可以临时打开,但最终测试最好回到业务可见结果。
## 写段测试
```ts
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 本身更接近答案。前端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)
}
}
```前端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
}
}
```前端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()
```服务端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>)
})
```前端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` 中前端5月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` 包起来。
```javascript
@action
async 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 里直接遍历大列表。
## 写段代码
```javascript
// 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; }
}
```