指南:自省 API 与 Spy 事件体系完全解析)
MobX 反应性分析Analyzing Reactivity指南自省 API 与 Spy 事件体系完全解析【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobxMobX 是一个简单、可扩展的状态管理库其核心是可观察状态 派生 反应的细粒度响应式模型。当调试复杂应用、排查为什么某个 autorun 没有触发、或者想要构建基于 MobX 的开发者工具时仅仅依赖console.log往往不够——你需要直接观察 MobX 内部运行时的依赖关系与事件流。本文以官方文档 docs/analyzing-reactivity.md 为主体结合本仓库packages/mobx的源码与测试系统讲解两组内省工具用于静态剖析依赖图的getDebugName/getDependencyTree/getObserverTree/getAtom自省 API以及用于实时监听全部运行时事件的spy全局监听器。读完本文你将能精确回答某个 observable 被谁观察、某个反应依赖了什么、一次 mutation 触发了哪些内部事件这类问题并能在自己的调试工具或 DevTools 插件中落地同样的技术。相关前置阅读本文频繁引用可观察对象与反应的概念可先参阅 docs/observable-state.md、docs/reactions.md文中事件对象的完整字段约定来自 docs/intercept-and-observe.md。一、内省 APIIntrospection APIs透视依赖图与命名MobX 在运行时维护着一张可观察节点Atom / ObservableValue / ComputedValue→ 派生节点Reaction / ComputedValue的双向依赖图。自省 API 的目标就是把这张不可见的图以可读形式暴露出来适用于调试期排查和构建上层工具例如官方 MobX DevTools 就使用了这些 API。此外isObservable*系列断言见 docs/api.md也常与之配合先判断对象类型再做内省。这些 API 的实现集中在 packages/mobx/src/api/extras.ts 与 packages/mobx/src/types/type-utils.ts并从 packages/mobx/src/mobx.ts 对外导出。测试用例见 packages/mobx/tests/base/extras.js。1.1getDebugName(thing, property?)签名getDebugName(thing, property?)作用返回可观察对象、属性、反应等的自动生成的可读调试名称。MobX DevTools 即依赖它来渲染节点标签。名称由构造器名加全局自增编号构成命名规则可以从 packages/mobx/tests/base/extras.js 的names、get debug name两个测试用例中归纳出来输入返回示例说明observable.box(3)ObservableValue1盒子boxed值observable({ a: 3 })整个对象ObservableObject2不传property时取对象管理节点observable({ a: 3 })aObservableObject2.a传入属性名时取该属性原子observable.map({ a: 3 })ObservableMap3Map 整体observable.map(...)aObservableMap3.a已有条目observable.map(...)b仅用于has判断的键ObservableMap3.b?问号后缀表示存在性原子hasMapobservable([1, 2])ObservableArray4数组computed(() ...)ComputedValue5计算值autorun(...)/reaction(...)Autorun6/Reaction…反应命名细节值得注意命名函数优先autorun(function namedFunction(){})会直接使用namedFunction作为名字见测试names。action 特殊处理getDebugName对 action 直接返回thing.nametype-utils.ts。测试getDebugName(action)表明匿名 action 返回unnamed action命名函数返回函数名显式传名则返回自定义名。自定义名称observable、computed、reaction、autorun等的第二参数选项对象都支持{ name }测试User provided debug names are always respected验证了开发与生产构建中自定义名都会被保留。生产构建差异开发模式编号为n如ObservableObject1生产构建minified中编号与部分前缀会被移除例如ObservableObject.key见测试Default debug names - production。1.2getDependencyTree(thing, property?)签名getDependencyTree(thing, property?)作用返回一棵树包含给定 reaction / computation 当前所依赖的全部可观察项。实现逻辑extras.ts非常直观先通过getAtom(thing, property)拿到根节点再递归遍历节点的observing_列表并去重形成{ name, dependencies }的嵌套结构。测试treeD给出了最典型的验证场景const a m.observable.box(3) const b m.computed(() a.get() * a.get()) const c m.autorun(() b.get()) m.getDependencyTree(c[$mobx]) // 输出编号按实际运行递增 // { // name: Autorun3, // dependencies: [{ // name: ComputedValue2, // dependencies: [{ name: ObservableValue1 }] // }] // }注意两点其一computed 在尚未被观察时没有依赖测试中b刚创建时返回{ name: ComputedValue2 }不含dependencies字段——MobX 计算值默认惰性求值只有被读取才会建立依赖其二对observable.map的依赖会被拆分为多个节点例如测试中的ObservableMap4.keys()、ObservableMap4.temperature取值、ObservableMap4.temperature?与ObservableMap4.absent?has存在性检查这正对应 Map 内部将键集合、值、存在性分别建模为独立原子的实现。1.3getObserverTree(thing, property?)签名getObserverTree(thing, property?)作用返回一棵树包含正在观察给定 observable 的全部 reaction / computation方向与依赖树相反。实现上extras.ts通过hasObservers/getObservers定义于 packages/mobx/src/core/observable.ts取得观察者集合并递归展开。与上例对称测试treeD中m.getObserverTree(a) // { // name: ObservableValue1, // observers: [{ // name: ComputedValue2, // observers: [{ name: Autorun3 }] // }] // }依赖树与观察者树互为反向视图getDependencyTree回答这个反应依赖什么getObserverTree回答这个状态被谁依赖。1.4getAtom(thing, property?)签名getAtom(thing, property?)作用返回给定可观察对象、属性、反应等背后的底层 Atom。Atom 是 MobX 响应式的最小单元observable.box、对象的每个属性、Map 的每个条目、数组整体、computed、reaction 背后都各自有一个 Atom或继承自 Atom 的节点。不同输入的分发逻辑在 packages/mobx/src/types/type-utils.ts测试get atom覆盖了主要路径输入返回的节点类型备注observable.box(3)ObservableValue盒子值本身即原子observable({ a: 3 })aObservableValue对象属性原子observable({ a: 3 })不带属性抛错提示必须指定属性please specify a propertyobservable.map(...)不带属性AtomkeysAtom_Map 的键集合原子observable.map(...)aObservableValue条目值原子或存在性原子observable([1, 2])Atom数组的atom_observable([1, 2])0抛错数组不支持按索引取原子computed(() ...)ComputedValue计算值本身autorun(...)Reaction反应本身未观察对象的未知属性抛错提示no observable property b found…1.5 组合实战调试反应为何没有触发把四个 API 组合起来就能系统定位响应式失效问题。假设某个autorun没有按预期更新可按以下步骤排查import { autorun, observable, getDebugName, getDependencyTree, getObserverTree, getAtom } from mobx const state observable({ count: 0 }) const disposer autorun(() console.log(count is, state.count)) // 1. 这个反应叫什么用于定位代码 console.log(getDebugName(disposer)) // Autorun2 // 2. 它此刻依赖了什么——若此处看不到 count说明读取路径有误 console.log(getDependencyTree(disposer)) // 3. count 被谁观察——双向确认 console.log(getObserverTree(state, count)) // 4. 拿到底层原子确认其身份与状态 const atom getAtom(state, count) console.log(atom.name_, atom.isBeingObserved_)一个常见坑是在autorun回调中通过非响应式方式如缓存引用、untracked、或在闭包外提前解构读取了count此时getDependencyTree会立即暴露问题——依赖列表中根本不会出现ObservableObject1.count。另一个坑是直接对数组取getAtom(arr, 0)源码会抛出It is not possible to get index atoms from arrays因为数组的索引原子并不单独建模。二、Spy全局事件监听器如果说自省 API 是静态照片那么spy就是实时录像。它注册一个全局监听器接收MobX 内部发生的所有事件——相当于同时对全部 observable 挂上observe监听此外还能感知 action / reaction / computed 的执行过程。MobX DevTools 正是基于它构建的。2.1 基本用法签名spy(listener)返回值一个disposer函数调用后取消监听。生产构建行为spy在生产构建minified中是no-op——文档明确说明它会被压缩消除。源码 packages/mobx/src/core/spy.ts 印证非开发环境会打印[mobx.spy] Is a no-op in production builds警告并返回空函数在开发环境则把监听器推入globalState.spyListeners数组并返回一个once包装的注销函数。事件分发函数spyReport同样带__DEV__守卫spy.ts。因此所有 spy 调试都必须在开发构建下进行。官方文档给出的监听所有 action 的例子import { spy } from mobx const disposer spy(event { if (event.type action) { console.log(${event.name} with args: ${event.arguments}) } }) // 不再需要时 disposer()2.2 事件类型总览spy监听器每次收到一个事件对象通常至少包含type字段。默认由spy发出的事件类型如下完整字段约定见 docs/intercept-and-observe.mdTypeobservableKind其他字段是否有嵌套子事件action—name,object作用域/this,arguments[]是scheduled-reaction—name否reaction—name是error—name,message,error否add/update/remove/delete/splice见事件总览见 docs/intercept-and-observe.md是report-end—spyReportEnd: true,time?总执行时长 ms否其中report-end是某个先前以spyReportStart: true开头的事件的结束标记由此将事件组织成父事件 子事件的嵌套组并可能附带总执行时间。此外类型定义spy.ts还列出了IComputedDidChange/IObjectDidChange/IArrayDidChange/IMapDidChange/ISetDidChange/IValueDidChange/IBoxDidChange等变更事件它们与observe收到的事件对象完全一致。2.3 从源码看事件何时触发actionstartAction中先发spyReportStart({ type: ACTION, name, object, arguments })执行完毕后在endAction中发spyReportEnd({ time })packages/mobx/src/core/action.ts。因此一个 action 事件天然包裹着它内部所有状态变更事件。reaction / scheduled-reactionpackages/mobx/src/core/reaction.ts 中当反应被调度时发scheduled-reaction实际执行时先发spyReportStart({ name, type: reaction })trackDerivedFunction跑完后发spyReportEnd({ time })。数组/对象/Map/Set 等变更例如 packages/mobx/src/types/observablearray.ts 在notifyHasObservers/ splice 逻辑中以spyReportStart(change)包裹真实变更随后atom_.reportChanged()并通知observe监听器。report-end的time字段取自Date.now() - startTime可用于粗粒度性能分析文档指出可能报告总执行时间。2.4 用快照测试观察真实事件流仓库测试 packages/mobx/tests/base/spy.js 与快照 packages/mobx/tests/base/snapshots/spy.js.snap 完整记录了真实事件序列。以spy error用例为例当 autorun 中 computed 抛出异常时事件流呈现清晰的嵌套结构{ type: reaction, name: autorun, spyReportStart: true } ← 反应开始 { type: update, observableKind: computed, newValue: CaughtException{ cause: Oops }, ... } { type: error, name: autorun, message: [mobx] Encountered an uncaught exception..., error: Oops } { type: report-end, spyReportEnd: true } ← 反应结束 { type: action, name: setX, arguments: [4], spyReportStart: true } ← action 开始 { type: update, observableKind: object, name: x, newValue: 4, oldValue: 3, ... } { type: report-end, spyReportEnd: true }几个有价值的细节action事件的对象字段object指向正确的this作用域即使 action 被解构后调用也如此——测试bound actions report correct object验证了makeAutoObservable的autoBind行为。computed 的update事件只在值真正变化时发出——测试computed shouldnt report update unless the value changed #3109证明对偶数递增两次isEven值不变事件队列中取不到update。这符合 computed 基于结果缓存的语义。spy监听器内部可以安全地注销自己测试spy stop listen from handler, #1459注销基于globalState.spyListeners的过滤。2.5 典型应用日志中间件与调试工具基于spy编写一个简易的行为日志器把所有 action 及其耗时记录下来import { spy } from mobx const actionLog [] const disposer spy(event { if (event.type action) { actionLog.push({ name: event.name, args: event.arguments, time: Date.now() }) console.log([action] ${event.name}, event.arguments) } if (event.type error) { console.error([mobx error] ${event.message}) } })也可以组合spy与自省 API在scheduled-reaction事件到达时调用getDependencyTree(reactionNode)即可实现当某个反应被调度时顺带打印它的依赖快照这本质上是 DevTools 中dependency graph视图的简化版实现。三、实战建议与注意事项仅开发环境可用所有 spy 事件在__DEV__下才分发生产构建中spy是 no-opgetDebugName在生产构建下的名称也会失去编号细节如ObservableObject.key。调试请始终使用开发构建。自省 API 面向调试与工具getDependencyTree/getObserverTree依赖内部observing_/observers_结构属于内省性质业务代码中应优先使用autorun/reaction/computed这类声明式 API而不是轮询自省结果。理解惰性未被观察的 computed 没有依赖树先被读取、被观察后才建立依赖因此依赖树为空本身可能是一个有效状态而非 bug。事件顺序即执行顺序action 事件包裹其内部全部变更事件reaction 事件包裹求值过程利用spyReportStart/spyReportEnd配对即可还原一次用户操作引发的完整因果链。四、相关文档与源码索引官方文档docs/analyzing-reactivity.md、docs/intercept-and-observe.md、docs/api.md、docs/reactions.md自省 API 实现packages/mobx/src/api/extras.ts、packages/mobx/src/types/type-utils.tsSpy 实现与事件类型packages/mobx/src/core/spy.ts、packages/mobx/src/core/action.ts、packages/mobx/src/core/reaction.ts观察者集合与原子packages/mobx/src/core/observable.ts公共导出packages/mobx/src/mobx.ts测试用例packages/mobx/tests/base/extras.js、packages/mobx/tests/base/spy.js、packages/mobx/tests/base/snapshots/spy.js.snap【免费下载链接】mobxSimple, scalable state management.项目地址: https://gitcode.com/gh_mirrors/mo/mobx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考