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 的快照。另一个边界是对象本身没有被 makeAutoObservableobservable 或对应 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 的自动追踪越准确,页面也越不容易出现莫名其妙的刷新。

标签:ReactMobx