ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Formily 响应式核心 API 实战解析:reaction 的脏检查与依赖订阅机制

Formily 响应式核心 API 实战解析:reaction 的脏检查与依赖订阅机制 Formily 响应式核心 API 实战解析reaction 的脏检查与依赖订阅机制【免费下载链接】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导读reaction是 Formily 响应式核心包formily/reactive中与autorun并列的副作用响应 API。它接收一个 tracker追踪函数与一个 subscriber回调响应函数当 tracker 内部消费的 observable 数据发生变化时tracker 会重复执行但与autorun不同的是只有当 tracker 的返回值发生变化时subscriber 才会被触发。这一“先比较返回值、再决定是否回调”的语义让reaction天然适合做“数值变化驱动的副作用”场景例如监听表单值求和、筛选条件汇总、分页数据变化等。读完本文你将掌握reaction的完整签名、三个可选参数的精确行为、与batch协作时的触发时机以及它在源码中的执行链路与测试验证依据。reaction 是什么与 autorun 的定位差异在formily/reactive中autorun与reaction同源但语义不同autoruntracker 内部任何被消费的 observable 发生变化tracker 就会重新执行没有返回值比较环节适合需要“每次都重跑”的渲染型副作用reactiontracker 重新执行之后还会对返回值做一次脏检查dirty check返回值不变则不触发 subscriber适合“只在计算结果变化时才做副作用”的场景。两者都返回一个dispose函数用于解除订阅。reaction的 API 文档位于 packages/reactive/docs/api/reaction.zh-CN.md其核心思想可用一句话概括tracker 负责收集依赖并产出值subscriber 只关心这个值是否真的变了。签名与参数详解reaction的完整 TypeScript 签名如下出自 API 文档interface IReactionOptionsT { name?: string equals?: (oldValue: T, newValue: T) boolean //脏检查 fireImmediately?: boolean //是否第一次默认触发绕过脏检查 } interface reactionT { ( tracker: () T, subscriber?: (newValue: T, oldValue: T) void, options?: IReactionOptionsT ): void }各参数含义参数类型说明tracker() T追踪函数。执行期间读取的 observable 属性会被自动收集为依赖其返回值作为“观察值”参与脏检查subscriber(newValue, oldValue) void回调响应函数。仅在 tracker 返回值发生变化时执行入参分别为新值、旧值options.namestring反应名称默认值为Reaction主要用于调试定位options.equals(oldValue, newValue) boolean自定义脏检查函数返回true表示“相等”不触发返回false表示“不等”触发。默认使用!严格不等比较options.fireImmediatelyboolean是否在创建时立即触发一次 subscriber绕过脏检查接口定义同样存在于源码 packages/reactive/src/types.ts其中name的默认值Reaction可以在 autorun.ts 的实现中看到const realOptions { name: Reaction, ...options, }从官方用例看运行机制文档给出的完整用例这里补充了逐步执行说明import { observable, reaction, batch } from formily/reactive const obs observable({ aa: 1, bb: 2, }) const dispose reaction(() { return obs.aa obs.bb }, console.log) batch(() { //不会触发因为 obs.aa obs.bb 的值没变 obs.aa 2 obs.bb 1 }) obs.aa 4 dispose()这段代码的执行时序如下创建阶段reaction创建时会立即执行一次 tracker此时obs.aa obs.bb 3同时完成对obs.aa、obs.bb两个属性的依赖收集由于未传fireImmediatelysubscriber 不会立刻执行。batch 阶段batch内的两次赋值obs.aa 2、obs.bb 1会被合并处理。虽然两次赋值都触发了依赖通知但由于处于批处理中reaction只会被压入待执行队列批处理结束时统一执行一次 tracker此时返回值依然是2 1 3与旧值3相等脏检查不通过subscriber 不触发。单独赋值阶段obs.aa 4在 batch 之外执行tracker 重新计算得到4 1 5与旧值3不等subscriber 被调用console.log(5, 3)新值、旧值。销毁阶段dispose()解除订阅此后任何对obs.aa、obs.bb的修改都不会再触发 tracker 与 subscriber。源码实现reaction 的完整执行链路reaction的实现位于 packages/reactive/src/autorun.ts与autorun同文件共享同一套反应调度基础设施见 packages/reactive/src/reaction.ts。其内部由三个关键部分构成。1. tracker 的封装与依赖收集const reaction: Reaction () { if (ReactionStack.indexOf(reaction) -1) { releaseBindingReactions(reaction) // 先释放旧的依赖绑定 try { ReactionStack.push(reaction) // 压入反应栈进入收集态 value.currentValue tracker() // 执行 tracker 并记录返回值 } finally { ReactionStack.pop() // 退出收集态 } } }执行 tracker 期间任何对 observable 属性的读取都会触发 bindTargetKeyWithCurrentReaction把“当前反应 目标属性键”写入RawReactionsMap依赖表而每次重新执行前调用releaseBindingReactions清空旧依赖从而保证依赖集合总是反映最近一次 tracker 的实际读取路径。2. 调度器与脏检查reaction._scheduler (looping) { looping() // 重新执行 tracker更新 currentValue if (dirtyCheck()) fireAction() // 脏检查通过才触发 subscriber value.oldValue value.currentValue // 更新旧值快照 }其中脏检查逻辑为const dirtyCheck () { if (isFn(realOptions.equals)) return !realOptions.equals(value.oldValue, value.currentValue) // 自定义比较 return value.oldValue ! value.currentValue // 默认 ! 比较 }也就是说默认使用!做“引用/原始值”比较对于对象、数组这类引用类型即使内容完全一致只要引用变了也会触发——这正是官方用例与测试中“浅比较”的行为来源。3. subscriber 的触发与销毁const fireAction () { try { batchStart() // 包裹在批处理中执行避免重复触发 if (isFn(subscriber)) subscriber(value.currentValue, value.oldValue) } finally { batchEnd() } }reaction最终返回销毁函数return () { disposeBindingReactions(reaction) // 从 RawReactionsMap 中移除所有依赖绑定 }销毁函数调用 disposeBindingReactions将反应标记为_disposed并从全局依赖表中彻底解除之后对相关属性的任何修改都不会再唤醒该反应。关键语义深入脏检查Dirty Check的边界reaction的触发条件是“tracker 返回值变化”而非“依赖数据变化”。二者的区别体现在两个典型场景依赖数据变了但返回值没变 → 不触发官方用例中的 batch 场景依赖数据没变但返回值变了 → 不可能发生依赖未变则 tracker 不会重新执行。测试 packages/reactive/src/tests/autorun.spec.ts 中的reaction dirty check用例对此有精确验证aa被标记为observable.ref后在batch中连续两次赋相同的值123handler 调用次数保持为 0。自定义 equals 实现深度比较默认!是浅比较如果 tracker 返回的是对象需要按内容比较时可自定义equalsreaction( () { return obs.aa // aa 是 observable.ref 引用类型 }, handler, { equals: (a, b) JSON.stringify(a) JSON.stringify(b), // 深度比较 } ) obs.aa { bb: 123 } // 内容相同equals 返回 true不触发对应测试reaction with deep equals赋值一个内容相同的新对象引用后 handler 调用次数为 0而reaction with shallow equals用例中不传equals时同样赋值新引用handler 调用次数为 1——直观展示了浅/深比较的差异。fireImmediately绕过脏检查立即触发reaction( () obs.aa.bb, handler, { fireImmediately: true } ) // 创建后 handler 立即被调用 1 次即使旧值等于新值测试reaction fireImmediately验证创建即调用 1 次随后赋相同值不触发赋新值再触发 1 次。源码中的实现顺序为先执行一次 tracker 并同步oldValue currentValue然后调用fireAction()——因此即使新旧值相等也会被强制执行一次 subscriber。subscriber 内部读取 observable 不建立新依赖subscriber执行时ReactionStack已清空tracker 的执行与回调调用是分离的因此在 subscriber 内读取 observable 不会产生新的依赖绑定。测试reaction untrack handler证明了这一点handler 内部读取obs.aa.cc随后修改obs.aa.cc并不会导致 handler 被再次调用。依赖的动态重收集tracker 每次执行前都会释放旧依赖因此依赖集合可以随条件分支动态变化。测试reaction recollect dependencies展示了典型场景reaction(() { if (obs.aa aaa) { return obs.bb // 当前只依赖 obs.aa、obs.bb } return obs.cc // 分支切换后改为依赖 obs.aa、obs.cc }, trigger)当obs.aa被改为111后tracker 走第二个分支依赖自动切换为obs.cc后续修改obs.bb不再触发修改obs.cc才会触发。与 autorun 的对比速查维度autorunreactiontracker 返回值无返回值参与判断返回值参与脏检查触发条件依赖变化即重跑依赖变化且返回值变化才回调subscriber 入参无tracker 即副作用(newValue, oldValue)典型场景渲染、日志、同步写缓存按计算结果驱动的副作用求和、过滤、分页返回销毁函数✅✅autorun的文档见 packages/reactive/docs/api/autorun.zh-CN.md它与reaction共享ReactionStack、RawReactionsMap、批处理队列等基础设施。与 batch 协作的实践要点reaction内部对 subscriber 的调用本身被batchStart/batchEnd包裹见fireAction因此 subscriber 内部若再次修改 observable不会引发同步的连锁反应而是进入批处理队列统一消化在batch外部连续修改多个依赖每次修改都会触发一次 tracker 执行与脏检查多次回调若希望合并为一次回调请像官方用例那样把修改包进batch批处理结束时反应只会执行一次详见 packages/reactive/docs/api/batch.zh-CN.md在 Formily 表单体系中reaction常被用于监听若干字段值的变化并汇总计算结果如总价、依赖联动值通过equals可避免对复杂计算结果的重复回调。小结reaction是formily/reactive中最具“计算语义”的响应式 APItracker 承担依赖收集与取值职责脏检查决定了 subscriber 的触发边界fireImmediately、equals则提供了对触发时机的精细控制。理解其与batch的协作时序和ReactionStack依赖收集机制是正确使用 Formily 响应式体系、避免“多触发”或“漏触发”的关键。以上行为均有对应源码autorun.ts、reaction.ts与单元测试autorun.spec.ts作为依据读者可结合实际场景进一步验证。【免费下载链接】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),仅供参考
返回列表