前端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)
}
}
```标签
Mobx
MobX是一个基于信号的、经过实战测试的库,通过透明地应用函数式响应式编程,使状态管理变得简单和可扩展。

前端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` 中