formily/reactive untracked 完全指南为响应式追踪构建不被收集的读取边界【免费下载链接】formily Cross Device High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3项目地址: https://gitcode.com/gh_mirrors/fo/formilyuntracked是formily/reactive提供的一个边界函数Boundary Function它允许开发者在响应式追踪如autorun的依赖收集进行期间将某一段同步代码内的所有可观察属性读取排除在依赖收集之外。本文以 untracked.md 为骨架结合packages/reactive的源码实现与测试用例完整讲解其签名、用法、底层原理及与batch、action、computed的协作关系。读完你将能够在 Formily 表单模型或任意基于formily/reactive的应用中精准控制哪次读取不该触发重执行写出性能更可控的响应式逻辑。认识 untracked一个只读不追踪的边界官方文档对untracked的描述非常简洁Usage is similar to batch, and will never be collected by dependencies within a given untracker function即用法与batch相似在给定的 untracker 函数内部任何可观察对象的读取都永远不会被依赖收集器收集。换句话说当你在autorun或reaction、Tracker的追踪函数里用untracked包裹某段读取逻辑时这段逻辑读取的字段不会成为当前响应式任务Reaction的依赖。后续这些字段无论怎么变化都不会触发该任务的重新执行。这与batch有本质区别batch解决的是通知时机问题——延迟副作用执行、批量合并通知untracked解决的是依赖关系问题——直接切断读取与依赖的绑定。两者在formily/reactive中是正交的两个开关分别对应BatchCount与UntrackCount两个计数器。函数签名与参数说明文档给出的类型签名如下interface untrackedT extends () any { (untracker?: T): ReturnTypeT }要素说明参数untracker可选的回调函数类型为() any。传入后会在取消追踪上下文中同步执行不传则为空操作内部不做任何事返回值ReturnTypeT即untracker回调的返回值会被原样透传便于在表达式中内联使用调用方式untracked(() ...)或untracked()无参调用同样安全参数是可选的这一点在测试中得到了直接验证untracked.spec.ts 中专门有一条no params untracked用例直接调用untracked()不抛异常。从源码看untracked.ts 的实现只有短短几行import { createBoundaryFunction } from ./internals import { untrackStart, untrackEnd } from ./reaction export const untracked createBoundaryFunction(untrackStart, untrackEnd)它由通用的createBoundaryFunction工厂构造核心行为是进入时start执行回调无论成功失败最后end保证了计数必然成对增减详见下文原理章节。基础示例让 autorun 忽略某次读取文档给出了如下示例我们逐行拆解import { observable, autorun, untracked } from formily/reactive const obs observable({ aa: 11, }) autorun(() { console.log(untracked(() obs.aa)) // 变化时不会触发 }) obs.aa 22执行流程分析observable({ aa: 11 })创建响应式代理对象autorun首次执行追踪函数此时obs.aa的读取发生在untracked(() obs.aa)内部由于读取发生在取消追踪上下文中依赖收集器没有把aa记到该 autorun 上因此obs.aa 22修改字段后console 不会再次输出——aa的变化与该 autorun 无关。仓库测试 untracked.spec.ts 用jest.fn()精确验证了这一行为test(basic untracked, () { const obs observableany({}) const fn jest.fn() autorun(() { untracked(() { fn(obs.value) }) }) expect(fn).toBeCalledTimes(1) // 首次执行调用一次 obs.value 123 expect(fn).toBeCalledTimes(1) // 修改后仍只有一次说明未被追踪 })这是一个典型的读取一次但不订阅的语义非常适合在追踪函数内读取不参与响应式依赖的配置值、元数据或调试信息。原理剖析UntrackCount 计数与依赖收集闸门要理解untracked为什么能切断依赖需要沿着调用链深入源码。第一步边界工厂的 try/finally 保证internals.ts 中createBoundaryFunction的实现export const createBoundaryFunction ( start: (...args: any) void, end: (...args: any) void ) { function boundaryF extends (...args: any) any(fn?: F): ReturnTypeF { let results: ReturnTypeF try { start() if (isFn(fn)) { results fn() } } finally { end() } return results } boundary.bound createBindFunction(boundary) return boundary }关键点有两个使用try ... finally包裹即使fn内部抛异常end()也一定会执行不会让全局的未追踪计数泄漏通过isFn(fn)判断非函数包括不传参时跳过执行仅完成start/end的空转。第二步全局计数器的增减start/end对应 reaction.ts 中的untrackStart/untrackEndexport const untrackStart () { UntrackCount.value } export const untrackEnd () { UntrackCount.value-- }UntrackCount是定义在 environment.ts 中的一个模块级共享状态export const ReactionStack: Reaction[] [] export const BatchCount { value: 0 } export const UntrackCount { value: 0 }UntrackCount.value 0即表示当前正处于取消追踪作用域内该状态由 reaction.ts 暴露为isUntracking()export const isUntracking () UntrackCount.value 0由于计数可叠加untracked支持任意层级的嵌套——每一层进入1、退出-1只有全部退出后才恢复追踪。第三步依赖收集器门口的闸门真正拦截依赖收集的地方在 reaction.ts 的bindTargetKeyWithCurrentReactionexport const bindTargetKeyWithCurrentReaction (operation: IOperation) { let { key, type, target } operation if (type iterate) { key ITERATION_KEY } const reactionLen ReactionStack.length if (reactionLen 0) return const current ReactionStack[reactionLen - 1] if (isUntracking()) return // ← 取消追踪时直接短路 if (current) { DependencyCollected.value true addReactionsMapToReaction(current, addRawReactionsMap(target, key, current)) } }这段逻辑是响应式系统对一次属性读取的公共处理入口由代理的 get 拦截器触发。当isUntracking()为真时函数直接returntargetkey就不会被登记到当前 Reaction 的依赖映射RawReactionsMap中。于是后续该字段变化时Reaction 的依赖比对里根本找不到这条记录自然也就不会触发重新执行——这正是untracked语义的最终落点。整个链路可以概括为untracked(fn) └─ untrackStart() → UntrackCount.value 进入未追踪域 └─ fn() → 读取触发 get 拦截器 └─ bindTargetKeyWithCurrentReaction └─ isUntracking() true → 短路不登记依赖 └─ untrackEnd() → UntrackCount.value-- 退出未追踪域untracked 与 batch、action 的关系文档明确提到用法与 batch 相似二者同源但有分工。我们对比源码来厘清batch只延迟、不切断batch.ts 的实现export const batch: IBatch createBoundaryAnnotation(batchStart, batchEnd) batch.scope createBoundaryAnnotation(batchScopeStart, batchScopeEnd) batch.endpoint (callback?: () void) { if (!isFn(callback)) return if (BatchCount.value 0) { callback() } else { BatchEndpoints.add(callback) } }batchStart/batchEndreaction.ts通过BatchCount.value的增减把batchEnd时产生的 pending reactions 延后到计数归零后才统一执行。也就是说batch 只是推迟了通知与重执行读取仍然会被收集为依赖只是重执行延后、合并了。actionbatch 与 untracked 的合体action.ts 是两者组合的最好例证export const action: IAction createBoundaryAnnotation( () { batchStart() untrackStart() }, () { untrackEnd() batchEnd() } )action在进入时同时开启batchStart与untrackStart退出时对称关闭。从源码结构看可以推断action 的本质就是批量 不追踪的组合边界——既合并通知又让 action 内部对可观察对象的读取不参与外部依赖收集。三者对比边界函数依赖是否被收集副作用通知典型用途untracked否isUntracking()短路立即读取一次性数据、配置、元数据batch是延后合并BatchCount归零后统一执行连续修改多个字段减少重执行次数action否延后合并事件回调/提交逻辑中批量改值同时避免外部追踪进阶场景computed 在 untracked 中的行为untracked不仅影响普通字段读取还影响计算属性computed的取值方式。在 computed.ts 的get()中可以看到function get() { if (hasRunningReaction()) { bindComputedReactions(reaction) } if (!isUntracking()) { // 如果允许 untracked 过程中收集依赖那么永远不会存在绑定因为 _dirty 已经设置为 false if (reaction._dirty) { reaction() reaction._dirty false } } else { compute() } bindTargetKeyWithCurrentReaction({ target: context, key: property, type: get, }) return store.value }含义是在正常追踪上下文中读取 computed 会执行其内部依赖收集并缓存而在untracked作用域内computed 会跳过自身的 Reaction 执行包括内部依赖的绑定与缓存更新直接调用compute()现场计算一次值。这在语义上是合理的——既然外部不收集依赖那么 computed 自身维护的 dirty 缓存、依赖绑定在这个上下文里也失去了意义直接求值最省开销。仓库测试 annotations.spec.ts 验证了在 untracked 中读取 computed 不会被收集test(computed no track get, () { const obs observable({ aa: 123 }) const compu observable.computed({ get: () obs.aa }) untracked(() { expect(compu.value).toBe(123) // 值可正常读出 }) })使用注意事项与最佳实践结合源码实现使用untracked时有以下几点值得注意回调必须是同步函数。依赖收集的开启与关闭依赖UntrackCount计数而untrackStart/untrackEnd在同一个同步调用栈内对称执行异步操作中的读取无法被untracked覆盖。异常安全已由实现保证。createBoundaryFunction使用try ... finally回调抛错也会正确执行untrackEnd()不会污染后续代码的追踪状态——因此不需要手动兜底 try/catch。无参调用安全。untracked()等价于一次空转的 start/end可用于占位或条件化包装测试用例已覆盖。支持嵌套。UntrackCount是计数器而非布尔值多层untracked嵌套可以正常工作最外层退出后恢复追踪。读取 computed 时行为不同。如前文所述untracked内读取 computed 会走直接计算分支若你的 computed 内部有昂贵计算且希望复用缓存应避免在untracked中读取它。可作为依赖收集的性能工具。在autorun/reaction的追踪函数里用untracked包裹不参与响应的读取如临时中间量、仅用于分支判断的全局配置可以减少无效依赖避免字段频繁变化引发不必要的重执行。.bound变体。从 internals.ts 的实现可以看到untracked上还挂载了bound变体createBindFunction用于将回调与指定context绑定后在边界内执行它是边界函数工厂的通用能力需要绑定this场景时可以使用。总结untracked是formily/reactive响应式体系中最基础的边界原语之一它通过UntrackCount计数器配合isUntracking()短路在bindTargetKeyWithCurrentReaction这一依赖收集的唯一入口处精准拦截实现读取但不订阅。它与batch延迟通知正交并与batch组合构成了action的底层实现对 computed 则退化为直接计算。无论是优化autorun的依赖集合还是实现只读一次的配置读取untracked 的实现源码、边界工厂与测试用例都值得作为深入 Formily 响应式内核的第一份参考资料。【免费下载链接】formily Cross Device High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3项目地址: https://gitcode.com/gh_mirrors/fo/formily创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
