5月27日 18:16

MobX 组件不更新、异步报错怎么办?常见坑和解决方案

MobX 的响应式机制看起来很简单——observable 包数据、observer 包组件、action 改状态,三件套一配就完事了。但实际项目里踩坑的地方不少:组件明明包了 observer 却不更新、异步操作改了状态没反应、computed 值死活不刷新。这篇文章把最常遇到的坑按出现频率排了一遍,每个坑讲清楚为什么掉进去以及怎么爬出来。

组件包了 observer 却不更新

这是用 MobX 最容易遇到的问题,没有之一。现象很明确:数据变了,组件纹丝不动。原因通常就那么几个。

没有真正访问 observable 属性。 observer 只追踪 render 过程中实际读取的 observable 属性。如果你在 render 外面把值解构出来,MobX 根本不知道这个组件依赖那个属性:

javascript
// 不会更新——render 里没有直接访问 observable const { count } = store; return <div>{count}</div>; // 会更新——render 过程中访问了 store.count return <div>{store.count}</div>;

用了普通对象而非 observable。 这看起来很低级,但在项目里经常出现——某个同事新加了一个对象,忘了用 observable 包一下,组件读它当然不会更新。

MobX 6 没开 action 就改状态。 MobX 6 默认 enforceActions: 'observed',意味着所有 observable 状态的修改必须在 action 里进行。在 action 外直接 this.count++ 会报错。如果你为了省事关了这个检查(configure({ enforceActions: 'never' })),表面上看不出问题,但 MobX 内部的批量更新机制会失效,导致多次修改触发多次渲染。

javascript
class Store { count = 0; constructor() { makeAutoObservable(this); } increment() { this.count++; // makeAutoObservable 自动把方法变成 action } }

makeAutoObservable 就不用手动标注 action 了,它会自动推断。

render 里创建了新的引用类型。 每次 render 都 new 一个对象或数组,即使内容一样引用也不同,React 做 shallow compare 会认为 props 变了,触发不必要的子组件重渲染。更隐蔽的问题是,如果你把这个新对象传给另一个 observer 组件,MobX 会误以为依赖变了。

异步操作改了状态不生效

@action 只能同步地修改状态。一旦遇到 await,action 的边界就断了——await 之后的代码不在 action 作用域内,MobX 会在控制台疯狂报警告。

三种解决方案,推荐程度从高到低:

用 flow(最推荐)。 flow 是 MobX 专门为异步设计的,用 generator 函数写,每个 yield 之间的代码自动包裹在 action 里,心智负担最小:

javascript
class Store { data = null; loading = false; constructor() { makeAutoObservable(this); } fetchData = flow(function* () { this.loading = true; try { const res = yield fetch('/api/data'); this.data = yield res.json(); } catch (e) { console.error(e); } finally { this.loading = false; } }); }

用 runInAction。 如果不想用 generator,在 await 之后手动把状态修改包进 runInAction:

javascript
async fetchData() { this.loading = true; try { const res = await fetch('/api/data'); runInAction(() => { this.data = res.json(); }); } finally { runInAction(() => { this.loading = false; }); } }

注意 runInAction 需要从 mobx 导入,而且每次修改状态都要包一次,容易漏。

用 action 包裹整个 async 函数再配合 runInAction。 这是上面两种的混合,不推荐,代码更啰嗦。

computed 值不更新或更新不对

computed 有两个大坑:一个是在里面搞副作用,一个是依赖追踪丢了。

computed 里搞副作用。 computed 本质是一个缓存计算值,MobX 期望它是纯函数——给同样的输入返回同样的输出,不做任何额外的事。如果你在 computed 里调接口、打日志、改其他状态,MobX 的缓存策略会乱套:

javascript
// 大错特错 get filteredList() { console.log(this.items.length); // 副作用 fetch('/api/track', { body: this.query }); // 副作用 return this.items.filter(i => i.active); } // 正确——纯计算 get filteredList() { return this.items.filter(i => i.active); }

需要副作用的场景用 autorunreaction,别用 computed。

依赖追踪丢了。 MobX 的响应式只在属性被实际读取时才建立追踪。常见写法是解构了 observable 再用,或者条件分支里读了一个属性但返回值没用到:

javascript
// 不会追踪 this.data get bad() { const data = this.data; // 读了但没用 return this.items.length; } // 会正确追踪两个依赖 get good() { return this.data.length + this.items.length; }

内存泄漏:reaction 没清理

autorunreactionwhen 这些函数调用后都会返回一个 disposer,组件卸载时必须调用。忘了清理的话,组件都销毁了,reaction 还在跑,轻则内存泄漏,重则操作已卸载组件的 DOM 报错。

React 项目里用 useEffect 的 cleanup 来处理:

javascript
useEffect(() => { const dispose = autorun(() => { console.log(store.count); }); return () => dispose(); }, []);

when 也要清理——虽然 when 会在条件满足后自动清理,但组件卸载时条件可能还没满足,reaction 还在等。

性能问题:渲染太频繁

MobX 的 observer 做得已经很精细了——只有组件实际读取的 observable 变了才会重渲染。但还是会遇到性能问题,常见原因:

单个组件读太多 observable。 一个大组件读了 store 里的十几个属性,其中任何一个变了都会重渲染整个组件。解法是拆组件——每个小组件只读自己关心的那几个属性:

javascript
// 一个大组件读了很多数据,任何一个变了都重渲染 const Dashboard = observer(() => ( <div> <p>{store.user.name}</p> <p>{store.settings.theme}</p> <p>{store.stats.count}</p> </div> )); // 拆成小组件,各管各的 const UserName = observer(() => <p>{store.user.name}</p>); const Theme = observer(() => <p>{store.settings.theme}</p>); const Count = observer(() => <p>{store.stats.count}</p>);

列表渲染没做细化。 如果列表组件整体用 observer 包裹,修改一条数据的某个字段会导致整个列表重渲染。给每条数据单独包一个 observer 组件,MobX 就能做到只更新变化的那一条。

装饰器配置问题

装饰器 @observable@action@computed 需要 Babel 或 TypeScript 的装饰器插件支持,配置稍微不对就报错。而且 ECMAScript 装饰器提案改了好几版,Babel 的 legacy: true 对应的是旧版提案,和 TypeScript 的 experimentalDecorators 也不是完全一回事。

MobX 6 开始官方推荐 makeAutoObservable / makeObservable,不再需要装饰器:

javascript
class Store { count = 0; list = []; constructor() { makeAutoObservable(this); } // 普通方法自动变成 action increment() { this.count++; } // getter 自动变成 computed get double() { return this.count * 2; } }

makeAutoObservable 能推断大多数场景,但有个限制:子类继承时需要手动在子类构造函数里再调一次。makeObservable 更灵活但需要手动标注每个属性。

数组操作踩坑

MobX 6 默认用 Proxy 实现 observable,数组操作基本和原生数组行为一致,pushsplicemapfilter 都能正常触发更新。但有几个细节:

直接赋值替换数组。 在 MobX 6 的 Proxy 模式下直接赋值是可以的(this.items = newArray),但如果你在用旧版 MobX 或者关了 Proxy(useProxies: 'never'),需要用 replace()

javascript
@action replaceItems(newItems) { this.items.replace(newItems); // 旧版 MobX 安全写法 }

传给非 MobX 库时要转原生。 有些第三方库(如 Lodash 的某些方法、antd 的 Table dataSource)对 observable 数组的兼容性不好,传之前用 .slice()toJS() 转成普通数组:

javascript
import { toJS } from 'mobx'; lodashChain(toJS(this.items));

Array.isArray 在旧版返回 false。 Proxy 模式下没问题,但旧版 observable 数组不是真数组,Array.isArray(observable([1,2,3])) 返回 false。用 isObservableArray 检测或 .slice() 转换。

循环依赖导致无限更新

两个 store 的 computed 互相依赖对方的数据,改一个触发另一个重算,另一个重算又触发第一个重算,死循环。MobX 会检测到循环依赖并抛出错误,但有时候循环不是那么明显——比如 A 的 reaction 修改了 B 的数据,B 的 reaction 又修改了 A 的数据,这种间接循环更难定位。

解法是理清数据流方向,让依赖关系变成单向的。如果两个 store 确实需要共享数据,抽出一个更高层的 store 来管理共享状态,让两个子 store 都依赖父 store 而不是互相依赖。

调试手段

MobX 提供了几个调试工具,按实用程度排序:

trace():放在 computed 或 observer 的 render 里,控制台会打印这个计算/渲染依赖了哪些 observable,以及是否在某个 observable 变化时重新计算。trace(true) 会在 debugger 断点停下,方便逐步排查。

spy():全局监听所有 MobX 事件(action 执行、observable 修改、computed 重算、reaction 触发),适合定位"到底是什么触发了这次渲染":

javascript
import { spy } from 'mobx'; spy((event) => { if (event.type === 'action') { console.log('Action:', event.name); } });

getDependencyTree / getObserverTree:程序化地获取依赖关系树,可以判断某个 computed 依赖了哪些 observable,或者某个 observable 被哪些 observer 观察。

MobX DevTools:浏览器扩展,可视化展示 observable 树和依赖关系。功能不如 Redux DevTools 丰富,但对于排查响应式链路断裂够用了。

TypeScript 类型问题

makeObservable 的泛型参数容易写错。MobX 6 要求传入类型参数来推断 this 的类型:

typescript
class Store { count: number = 0; constructor() { makeObservable<Store>(this, { count: observable, increment: action, }); } increment() { this.count++; } }

如果用 makeAutoObservable,大多数情况不需要手动传泛型,TypeScript 能自动推断。但 makeAutoObservable 不支持子类,子类需要在构造函数里手动调 makeObservable 并列出所有需要观测的属性。

另一个常见问题是 observable 的类型推断——observable({ list: [] }) 里的 list 会被推断为 never[] 而不是 any[],需要手动标注类型:

typescript
class Store { list: Item[] = []; constructor() { makeAutoObservable(this); } }
标签:Mobx