尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Formily Reactive 性能最佳实践:深入理解 observable 包装粒度、computed 缓存、batch 批量与 dispose 资源释放
前端UI组件【免费下载链接】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点击查看免费下载formily/reactive 是 Formily 表单体系的响应式内核它为 React、Vue、React Native 等上层框架提供统一的依赖追踪与变更通知能力。本文围绕官方文档 packages/reactive/docs/guide/best-practice.zh-CN.md 归纳的四条最佳实践展开并结合仓库源码逐条剖析背后的实现原理帮助你在实际业务中写出性能更好、更不容易泄漏内存的响应式代码。读完本文你将掌握如何选择 observable 的包装粒度、如何用 computed 缓存派生计算结果、如何用 batch 合并多次赋值触发的更新以及如何正确释放 autorun/reaction 产生的副作用。一、四条最佳实践总览编号实践要点核心收益1尽量少用observable/observable.deep深度包装多用observable.ref/observable.shallow减少深层代理与递归开销提升读写性能2领域模型中多用computed计算属性智能缓存计算结果仅在依赖变化时惰性重算3尽量多用batch模式合并多次变更减少 Reaction 执行次数4使用autorun/reaction后记得调用dispose释放依赖绑定与副作用避免内存泄漏四条实践环环相扣前两条关注如何少建代理、少算重复值第三条关注如何少触发回调第四条关注如何及时收尾。下面逐条深入。二、实践一控制可观察粒度优先使用 observable.ref / observable.shallow2.1 四种包装注解的区别在 packages/reactive/src/observable.ts 中observable是一个同时具备多种注解能力的入口函数export function observableT extends object(target: T): T { return createObservable(null, null, target) } observable.box annotations.box observable.ref annotations.ref observable.deep annotations.observable observable.shallow annotations.shallow observable.computed annotations.computed也就是说observable(target)与observable.deep(target)等价都会对传入对象进行深度可观察化observable.shallow(target)只对当前一层做可观察化内层对象保持原样observable.ref(value)只包装一个值不递归、不代理内部结构访问方式为xxx.valueobservable.box(value)提供get()/set()形式的取值/赋值接口适用于需要封装复杂值的场景observable.computed(getter)创建惰性计算属性详见第三节。2.2 源码层面看深度 vs 浅层的开销差异深度与浅层包装的核心差异在 packages/reactive/src/internals.ts 的createObservable中体现export const createObservable (target, key, value, shallow?: boolean) { if (typeof value ! object) return value const raw ProxyRaw.get(value) ... buildDataTree(target, key, value) if (shallow) return createShallowProxy(value) if (isNormalType(value)) return createNormalProxy(value) if (isCollectionType(value)) return createCollectionProxy(value) return value }非shallow时会对对象/集合创建Proxy代理且后续每次访问嵌套属性时子对象也会被懒代理在baseHandlers/collectionHandlers的 get 拦截中递归走createObservable因此整棵对象树上的所有属性都会被纳入依赖追踪shallow模式只创建最外层的浅代理内层对象在读取时原样返回不再递归建立代理ref注解见 packages/reactive/src/annotations/ref.ts则根本不创建 Proxy只在内部保存一个store通过value属性 get/set 触发依赖绑定与 Reaction 通知开销最小。从 packages/reactive/src/annotations/observable.ts 可以看出 deep 模式对每次 set 也会再次调用createObservable重新包装新值function set(value: any) { const oldValue store.value value createObservable(target, key, value) // 深度模式新值也递归包装 store.value value ... }也就是说深度包装下任何一层对象的替换都会引发额外的递归包装开销而 shallow / ref 模式则跳过了这层递归。2.3 实践建议顶层状态如表单的字段模型、领域实体用observable或observable.deep是合理的因为内部结构需要整体联动响应对于只关心整个对象是否被替换、不关心其内部字段变化的场景例如大型配置对象、组件实例、不可变数据结构、第三方类实例优先用observable.shallow对于单一值字符串、数字、布尔、函数引用等或只需替换语义的引用直接使用observable.ref既能参与依赖追踪又避免无谓的代理开销若内层对象本身是Proxy或已被标记为 rawcreateObservable会直接返回原值因此重复包装不会叠加代理见 packages/reactive/src/internals.ts但主动控制包装粒度仍是降低运行开销的最佳手段。三、实践二领域模型多用 computed利用智能缓存避免重复计算3.1 computed 的两种用法computed既可以在define/model领域模型中作为字段注解使用也可以独立创建计算值import { observable, computed } from formily/reactive // 独立用法返回 { value } 形式 const fullName computed(() ${user.firstName} ${user.lastName}) fullName.value // 读取计算结果 // 领域模型用法配合 define/model 声明计算字段 const model define( { firstName: John, lastName: Doe }, { fullName: computed((self) ${self.firstName} ${self.lastName}), } )3.2 缓存与惰性重算的实现原理computed 的核心实现位于 packages/reactive/src/annotations/computed.ts其设计是一个带_dirty脏标记的迷你 Reactionreaction._scheduler () { reaction._dirty true // 依赖变化时只标记脏不立即重算 runReactionsFromTargetKey({ ... }) // 通知外层依赖 } ... function get() { ... if (reaction._dirty) { reaction() // 首次访问或依赖变化后才真正重算 reaction._dirty false } ... return store.value }由此可以总结出 computed 的三个关键特性智能缓存计算值被保存在store.value中在依赖未变化时反复读取不会重新执行计算函数直接返回缓存结果惰性重算依赖变化时只是把_dirty置为true真正的重算推迟到下一次被读取时进行。如果依赖变了但计算值一直没人读就不会产生多余计算级联失效computed 内部也是一个 Reaction读取它时hasRunningReaction()为真会通过bindComputedReactions与当前外层 Reaction 建立绑定因此外层依赖 computed 时computed 的失效会自动向上传播源码中的suspendComputedReactions会在无外层依赖时清理绑定见 packages/reactive/src/reaction.ts。3.3 实践建议在领域模型中凡是由多个可观察字段派生、且可能被多次读取的值如拼接名称、汇总金额、筛选后的列表、剩余额度等都应建模为computed而不是在每次 render 或每次读取时手动计算对比observable普通字段普通字段的赋值是写触发computed 是读惰性 依赖脏标记两者语义不同、适用场景不同依赖较多或计算较重的派生逻辑用 computed 可以在多次读取场景下获得接近 O(1) 的读取成本。四、实践三多用 batch 模式减少 Reaction 执行次数4.1 问题的由来一次赋值触发一次 Reaction默认情况下每一次对可观察状态的修改都会同步调度依赖它的 Reaction。在 packages/reactive/src/tests/batch.spec.ts 的no batch用例中可以看到const handler jest.fn() autorun(() { handler(obs.aa.bb) }) obs.aa.bb 111 obs.aa.bb 222 expect(handler).toBeCalledTimes(3) // 初始 1 次 两次赋值各 1 次连续两次赋值会触发两次 Reaction。如果在一个业务函数里连续修改多个字段就会产生大量重复执行——这正是 batch 要解决的问题。4.2 batch 的批量合并原理batch的实现见 packages/reactive/src/batch.tsexport const batch: IBatch createBoundaryAnnotation(batchStart, batchEnd) batch.scope createBoundaryAnnotation(batchScopeStart, batchScopeEnd) batch.endpoint (callback?: () void) { ... }其底层依赖 packages/reactive/src/reaction.ts 中的batchStart/batchEnd计数器机制export const batchStart () { BatchCount.value } export const batchEnd () { BatchCount.value-- if (BatchCount.value 0) { ... executePendingReactions() // 计数归零时统一执行所有排队中的 Reaction executeBatchEndpoints() ... } }也就是说batch通过计数器把一批变更期间触发的 Reaction 都放入PendingReactions队列直到最外层batchEnd计数归零时才批量执行一次。因此无论批内改了多少个值每个依赖这些值的 Reaction 最多执行一次并且只以最终状态为准。4.3 测试用例验证同文件batch用例packages/reactive/src/tests/batch.spec.ts对比了批内外行为obs.aa.bb 111 obs.aa.bb 222 expect(handler).toBeCalledTimes(3) // 批外两次赋值 两次 Reaction batch(() { obs.aa.bb 333 obs.aa.bb 444 }) expect(handler).toBeCalledTimes(4) // 批内两次赋值只再触发 1 次 expect(handler).lastCalledWith(444) // 且以批内最后一次赋值为准两次赋值放进同一个batch后handler 只多执行了一次且拿到的是最终值 444。4.4 batch 家族的其他形态batch(fn)/ 直接调用batch()同步包裹一段逻辑嵌套调用由计数器保证只在最外层结束时统一派发batch.bound(fn)返回绑定后的函数之后调用该函数时自动进入批量模式batch.bound相关用例见 packages/reactive/src/tests/batch.spec.ts适合作为事件处理函数或方法注入到对象上batch.scope(fn)即使外层没有 batch也会把 scope 内的 Reaction 延迟到 scope 结束时统一执行batchScopeEnd中批量派发PendingScopeReactions见 packages/reactive/src/reaction.tsbatch.endpoint(callback)注册一个批结束后回调若当前不在批内则立即执行否则等待批结束统一执行见 packages/reactive/src/batch.ts。4.5 实践建议凡是一次性修改多个可观察字段的业务方法提交表单、批量赋值、多字段联动、接口数据回填等都建议用batch包裹状态管理 action 或方法如果会被反复注入/绑定优先使用batch.bound封装需要所有更新落定后再执行一次性副作用如滚动到底部、统计、上报时用batch.endpoint。五、实践四使用 autorun/reaction 后必须调用 dispose避免内存泄漏5.1 autorun / reaction 都返回释放函数autorun与reaction的签名与返回值见 packages/reactive/src/autorun.tsexport const autorun (tracker: Reaction, name AutoRun) { ... reaction() // 立即执行一次追踪 return () { disposeBindingReactions(reaction) disposeEffects(reaction) cleanRefs() } } export const reaction (tracker, subscriber?, options?) { ... return () { disposeBindingReactions(reaction) } }两者的返回值都是一个释放函数dispose调用它之后该 Reaction 与所有可观察依赖之间的绑定关系会被解除后续再修改相关状态也不会触发回调。5.2 释放函数的底层行为释放函数的核心是 packages/reactive/src/reaction.ts 的disposeBindingReactionsexport const disposeBindingReactions (reaction: Reaction) { reaction._disposed true // 标记已释放 releaseBindingReactions(reaction) // 解除与所有目标 key 的绑定 suspendComputedReactions(reaction) // 挂起相关 computed 的绑定并置脏 }releaseBindingReactions会把 Reaction 从它依赖的每个可观察 key 的订阅者集合中移除此后这些 key 发生变化时不会再调度该 Reaction同时autorun的释放函数还会调用disposeEffectspackages/reactive/src/reaction.ts把autorun.effect注册的副作用一并批量释放。autorun.memo的缓存队列也会被cleanRefs清空避免残留引用。5.3 测试用例验证释放行为packages/reactive/src/tests/autorun.spec.ts 的autorun用例直接验证了这一点const dispose autorun(() { handler(obs.aa.bb) }) obs.aa.bb 111 expect(handler).toBeCalledTimes(2) dispose() // 释放绑定 obs.aa.bb 222 // 释放后再修改 expect(handler).toBeCalledTimes(2) // handler 不再被调用reaction用例同文件 L27-L44行为一致dispose()之后对obs.aa.bb的修改不再触发 subscriber。5.4 实践建议规范const dispose autorun(...)之后务必在组件卸载、页面销毁、任务结束时调用dispose()在 React/Vue 场景可分别借助useEffect清理函数、onUnmounted/beforeDestroy钩子调用 dispose长生命周期对象全局单例、服务层、领域模型内部创建的 autorun/reaction要在对象销毁时同步释放否则依赖图会随创建次数不断膨胀形成内存泄漏局部 autorun如果某个追踪只需要在局部作用域内生效也可以在逻辑结束处立即 dispose。六、总结四条实践的组合使用这四条最佳实践分别回答了响应式编程中的四个问题包装粒度用observable.ref/observable.shallow替代不必要的深度包装从源头降低代理与递归开销对应 packages/reactive/src/observable.ts 与 packages/reactive/src/internals.ts派生计算领域模型用computed缓存派生值依赖不变时读取零计算依赖变化时惰性重算对应 packages/reactive/src/annotations/computed.ts批量更新批量赋值放入batch或batch.bound/batch.scope让 Reaction 合并执行、以最终值为准对应 packages/reactive/src/batch.ts 与 packages/reactive/src/reaction.ts资源释放autorun/reaction创建后必须调用返回的dispose释放函数解除依赖绑定并回收副作用对应 packages/reactive/src/autorun.ts 与 packages/reactive/src/reaction.ts。在实际业务中通常的组合方式是领域模型以define/model声明字段与computed派生值状态修改方法用batch包裹外部通过autorun/reaction订阅状态并在生命周期结束处调用dispose。按照这套实践编写代码即可在 Formily 生态中无论是 React 还是 Vue 侧获得稳定、可预期的响应式行为与可维护的性能表现。赞分享前端UI组件【免费下载链接】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点击查看免费下载相关推荐Formily Reactive 最佳实践从 observable 深度包装到 dispose 释放的四大性能与内存要点Formily Reactive 最佳实践从 observable 深度包装到 dispose 释放的四大性能与内存要点 formily/reactive前端UI组件formily/reactive 核心概念深入解析Observable、Reaction、Computed 与批量更新实战formily/reactive 核心概念深入解析Observable、Reaction、Computed 与批量更新实战 formily/reactiv前端UI组件formily/reactive 核心概念深入解析Observable、Reaction、Computed 与 Batch 响应式编程模型formily/reactive 核心概念深入解析Observable、Reaction、Computed 与 Batch 响应式编程模型 导读 本文基于前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

Python学生成绩管理系统实战部署与避坑指南

Python学生成绩管理系统实战部署与避坑指南

简介:本资源是一套完整的Python学生成绩管理系统课程设计实践包,面向计算机专业初学者、课程设计学生及Python入门开发者,聚焦软件工程全流程实践,解决从需求分析到部署运行的系统开发能力训练问题。压缩包共412个文件&#xff0c…

📅 2026/9/23 21:28:35
JAVA微信小程序商城源码:完整后台才是核心,从部署到改造全解析

JAVA微信小程序商城源码:完整后台才是核心,从部署到改造全解析

简介:这套JAVA微信小程序商城源码附带完整后台,适合具备一定Java基础、希望快速搭建微信商城小程序的开发者或初创团队。项目采用springmvcmybatisspringmavenmysql架构,前端基于H5和CSS3,后台使用bootstrap-ace技术,整…

📅 2026/9/23 21:28:35
DeepSeek多模态模型实战:从Transformer原理到微调部署

DeepSeek多模态模型实战:从Transformer原理到微调部署

简介:围绕DeepSeek模型多模态处理与应用的深度学习技术文档,面向自然语言处理与计算机视觉方向的研究者、工程师及技术团队,系统讲解其在文本理解、图像识别和多模态信息融合方面的实现原理与落地方法。这份技术资料以单个docx文档承载&#…

📅 2026/9/23 21:28:35
MORE NEWS

更多资讯

📰

Dopamine 实验统计汇总实战:深入解析 colab.utils.summarize_data 逐迭代数据聚合

机器学习深度学习 【免费下载链接】dopamine Dopamine is a research framework for fast prototyping of reinforcement learning algorithms. 项目地址: https://gitcode.com/gh_mirrors/do/dopamine 点击查看 免费下载 导读:本文围绕 Dopamine 强化…

📰

EMQX MQTT 桥接陈旧连接状态修复解析:从「假 Connected」到真实健康检查与自动重连

后端物联网消息队列通信 【免费下载链接】emqx The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles 项目地址: https://gitcode.com/gh_mirrors/em/emqx 点击查看 免费下载 本文围绕 EMQX 开源仓库中 changes/ee/fix-15603.en.…

📰

CAD软件有哪些?建筑、机械、协同各领域代表软件梳理

CAD(Computer Aided Design,计算机辅助设计)软件是工程设计领域不可或缺的核心工具,广泛应用于建筑设计、机械制造、土木工程、电子电气、产品研发等众多行业。其核心目的是通过数字化手段提升设计效率、保证设计精度、便于协同与…

📰

AI陪伴机器人DTO参数校验-把脏数据拦在门口

07-DTO参数校验-把脏数据拦在门口系列:AI 伙伴(AI-Partner)——具身智能陪伴机器人 数据接口部署与二次开发篇(07/12)一、为什么校验要放在"门口" 先说个真实场景:你在前端页面上提交一条健康记…

📰

海信电视秋季新品打出王炸,双7旗舰首发,RGB-Mini LED电视体验常用常新

9月23日,2026海信电视召开秋季新品发布会,正式推出全新一代RGB-Mini LED影像旗舰U7T Pro、性能旗舰E7S Pro两大双7旗舰新品,一次性落地六大行业首发技术,从背光硬件、屏幕素质、专属画质到智能系统全方位革新,彻底改写…

📰

Python coding + ML + general coding ability

# Linked List(链表)面试知识体系与记忆模板> 核心原则:**Array 用 index;Linked List 用 pointer。**>> 链表题的核心不是“访问元素”,而是“移动和重新连接节点”。---## 1. 基本结构texthead↓[1] → [2]…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬