5月31日 15:55

MobX 依赖追踪到底是怎么知道该更新谁的?

MobX 依赖追踪的核心可以用一句话概括:谁在运行时读了 observable,谁就会被登记为它的依赖;以后这个 observable 变了,只通知登记过的人。这里的“谁”可能是 autorunreactioncomputed,也可能是被 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 执行期间。

标签:Mobx