服务端阅读 05月31日 15:55
React 里 MobX observer 为什么能自动更新组件?
MobX 的 observer 不是简单地给组件加一个订阅开关,它会在组件渲染时记录“这次 render 到底读了哪些 observable”。之后只有这些被读过的状态变化,组件才会重新渲染。也就是说,observer 的关键不是“组件用了 store”,而是“组件在渲染期间访问了 store 的哪个字段”。在 React 项目里,函数组件通常使用 mobx-react-lite,类组件才会用到 mobx-react。MobX 6 以后更推荐 makeAutoObservable,少写装饰器,也更适合 TypeScript 和现代构建环境。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 还能合并通知,减少组件中间态闪烁。async load() { this.loading = true; const data = await api.getList(); runInAction(() => { this.items = data; this.loading = false; });}observer 用得好,核心不是“所有组件都包一下”,而是让组件在正确的位置读取正确的 observable。状态读取越靠近展示位置,MobX 的自动追踪越准确,页面也越不容易出现莫名其妙的刷新。