多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Formily Reactive 的 observable 响应式对象创建 API 全解析

Formily Reactive 的 observable 响应式对象创建 API 全解析 前端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点击查看免费下载导读observable是 Formily 响应式核心库 formily/reactive 中最基础、使用频率最高的 API它用于创建具有不同响应式行为的对象同时也可以作为注解annotation标记类的响应式属性。本文将以 packages/reactive/docs/api/observable.md 为骨架逐一讲解observable/observable.deep、observable.shallow、observable.computed、observable.ref、observable.box五种形态的签名、适用场景与示例并结合 packages/reactive/src/observable.ts 与 packages/reactive/src/annotations 下的源码实现剖析它们各自的劫持策略与底层原理帮助你根据业务需求选择最合适的响应式容器。observable 家族概览五种形态与选择依据observable实际上是一个多态工厂它本身是函数同时又挂载了box、ref、deep、shallow、computed五个子方法。从 源码 可以看到它们之间的挂载关系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[MakeObModelSymbol] annotations.observable其中形态响应范围读写方式典型用途observable(obj)/observable.deep(obj)深劫持递归所有层级直接属性访问常规的嵌套对象 / 数组状态observable.shallow(obj)浅劫持仅第一层属性直接属性访问大数据对象只关心顶层变化observable.computed(fn)依赖追踪 缓存通过.value读取由其他响应式状态派生的计算值observable.ref(value)引用容器值整体替换通过.value读写包装基础类型number / string / booleanobservable.box(value)引用容器值整体替换通过.get()/.set()需要方法式读写的基础类型包装注意函数形式observable(obj)与observable.deep(obj)是等价的二者都指向深层劫持的注解实现见 annotations/observable.ts。deep只是语义更明确的一个别名。observable / observable.deep深层劫持响应式对象功能描述observable用于创建深层劫持的响应式对象。所谓深劫持指的是不仅目标对象自身的第一层属性是响应式的其嵌套的子对象、子数组在首次访问时也会被递归包装为响应式对象任何层级的读写操作都会被追踪和触发。签名interface observableT extends object { (target: T): T } interface deepT extends object { (target: T): T }入参为任意对象普通对象或数组返回值仍为原类型T实际返回的是原对象的响应式代理。使用示例import { observable, autorun } from formily/reactive const obs observable({ aa: { bb: 123, }, }) autorun(() { console.log(obs.aa.bb) }) obs.aa.bb 321运行上述代码时autorun中的副作用函数会立即执行一次并打印123当obs.aa.bb被修改为321时由于bb属于深层属性副作用会被重新触发并打印321。源码原理createObservable 的递归代理深层劫持的入口是 internals.ts 中的createObservable函数其核心逻辑可以归纳为三步原始值短路如果目标value不是对象typeof value ! object直接原样返回不对基础类型做包装重复包装保护通过ProxyRaw弱映射检查目标是否已被代理过若已代理则复用已有代理并维护数据树节点避免重复包装导致响应式失效构建数据树并创建代理调用buildDataTree将目标挂入全局数据树随后根据类型分别走createNormalProxy普通对象/数组走baseHandlers或createCollectionProxyMap/Set 等集合走collectionHandlers。深劫持的深体现在baseHandlers的get陷阱中当访问嵌套属性如obs.aa时如果该属性值是对象且尚未被代理处理器会递归调用createObservable将其包装后再返回。因此嵌套对象是按需惰性深度代理的——只有真正访问到的层级才会被包装这与 Vue 3 的 Proxy 实现思路一致。在tests/observable.spec.ts 中还有一组contains用例验证了深劫持下嵌套元素如arr[0].aa、element的子对象都能被数据树正确包含与追踪。observable.shallow浅层劫持响应式对象功能描述observable.shallow创建浅层劫持的响应式对象只响应目标对象第一层属性的读写操作。对于嵌套的子对象浅代理只保证替换整个子对象这一操作可被侦测子对象内部的属性变化不会被追踪。签名interface shallowT extends object { (target: T): T }使用示例import { observable, autorun } from formily/reactive const obs observable.shallow({ aa: { bb: 111, }, }) autorun(() { console.log(obs.aa.bb) }) obs.aa.bb 222 // 不会触发响应 obs.aa { bb: 333 } // 会触发响应示例中有意对比了两种写法的差异obs.aa.bb 222修改的是第二层属性浅劫持不会拦截副作用不会重新执行obs.aa { bb: 333 }替换第一层属性aa的整个引用浅劫持能够侦测到副作用重新执行。源码原理浅代理与父浅则子不代理规则浅劫持的实现位于 annotations/shallow.ts与深层实现几乎同构唯一区别是在调用createObservable(target, key, value, true)时传入第四个参数shallow true。createObservable内部由此分支见 internals.tsif (target) { const parentRaw ProxyRaw.get(target) || target const isShallowParent RawShallowProxy.get(parentRaw) if (isShallowParent) return value // 父级为浅代理时子对象不再代理原样返回 }这里有两个关键机制浅代理会通过RawShallowProxy弱映射登记见 internals.ts 中createNormalProxy/createCollectionProxy的shallow分支当某个浅代理对象的子属性被读取时baseHandlers在尝试递归代理前会先检查父级是否为浅代理——如果是则原样返回子对象不做包装。这正是obs.aa.bb 222无法触发响应的根本原因。适用建议当你的状态对象层级很深、但业务上只关心顶层结构变化时例如配置对象整体替换、列表整体更新使用shallow可以显著减少代理包装带来的开销。observable.computed带缓存的派生计算值功能描述observable.computed创建计算缓冲区calculation buffer它接受一个计算函数或包含get/set的对象返回一个带.value的响应式容器。计算值会自动追踪其内部读取的所有响应式依赖并具备惰性缓存能力依赖未变化时重复读取.value不会重复执行计算函数。签名interface computed { T extends () any(target: T): { value: ReturnTypeT } T extends { get?: () any; set?: (value: any) void }(target: T): { value: ReturnTypeT[get] } }签名支持两种重载传入纯函数{ value: ReturnTypeT }传入{ get?, set? }对象可同时定义读取逻辑与写入逻辑写入时set内部可以继续修改其他响应式状态实现可写计算属性。使用示例import { observable, autorun } from formily/reactive const obs observable({ aa: 11, bb: 22, }) const computed observable.computed(() obs.aa obs.bb) autorun(() { console.log(computed.value) }) obs.aa 33第一次读取computed.value时计算结果为3311 22并缓存当obs.aa变为33后computed 的依赖被标记为脏autorun再次读取时重新计算得到55控制台依次打印33与55。源码原理ComputedReaction 与脏标记computed 的实现是五个注解中最复杂的见 annotations/computed.ts核心可以拆解为几个部分计算反应compute()负责调用descriptor.get?.call(context)执行用户计算函数结果暂存于store.value依赖收集reaction()将自身压入ReactionStack后执行compute()这样计算函数内部读取的响应式状态就会把该反应登记为依赖reaction._name固定为ComputedReaction便于调试定位脏标记调度reaction._scheduler在依赖变化时仅置reaction._dirty true并广播计算值已过期不立即重算惰性求值get()中只有当reaction._dirty为true时才真正执行reaction()重算否则直接返回缓存的store.value读取时还会通过bindTargetKeyWithCurrentReaction把自己注册到当前正在运行的追踪者如autorun的依赖列表中批处理写入set()将descriptor.set的调用包裹在batchStart()/batchEnd()之间保证一次写入多个依赖时只触发一轮更新详见 packages/reactive/src/batch.ts。此外computed 注解在属性定义上做了性能优化getPropertyDescriptorCache会将类原型上的属性描述符按构造器缓存ClassDescriptorSymbol避免每次读取都递归遍历原型链。注意observable.computed必须与追踪上下文autorun、tracker、observe等配合使用才有意义。脱离追踪直接读取.value时由于没有消费者脏标记会在下次读取时才被重新计算即仍然正确只是不会主动求值。observable.ref引用劫持的响应式容器功能描述observable.ref创建引用劫持的响应式容器将任意值尤其是基础类型包装进一个带.value属性的容器中通过对ref.value的读写来驱动响应式更新。它解决了基础类型无法被直接代理的问题——基础类型不是对象无法挂载 getter/setter因此必须先包一层容器。签名interface refT extends object { (target: T): { value: T } }源码中的实际类型为IRefT(target: T): { value: T }见 annotations/ref.ts入参不限于objectobservable.ref(1)即传入了数字。使用示例import { observable, autorun } from formily/reactive const ref observable.ref(1) autorun(() { console.log(ref.value) }) ref.value 2控制台依次打印1与2。当ref.value被整体替换时绑定在容器上的副作用重新执行。源码原理数据树节点与响应式 value 属性ref 的实现位于 annotations/ref.ts。它创建了一个store内部存储对象并在返回的proxy上通过Object.defineProperty定义value属性的 getter/setterget()调用bindTargetKeyWithCurrentReaction把当前追踪者绑定到store.value上set(value)写入新值并在oldValue ! value时调用runReactionsFromTargetKey触发绑定在该键上的所有反应。同时它调用buildDataTree(target, key, store)将容器接入全局数据树并把proxy[ObModelSymbol] store挂上模型符号使容器与内部存储的映射关系可被框架识别。当 ref 作为类的注解使用时target/key存在会在原对象属性上直接定义 getter/setter 并返回原对象见 annotations/ref.ts。ref 与直接使用对象的区别对observable.ref(1)而言追踪的粒度是整个值而不是值的内部属性——这也正是引用劫持的含义。observable.boxget/set 方法式读写的响应式容器功能描述observable.box与observable.ref功能类似都是对基础类型等值做响应式包装区别在于box 通过get()/set()方法读写数据而不是访问.value属性。签名interface boxT extends object { (target: T): { get: () T; set: (value: T) void } }使用示例import { observable, autorun } from formily/reactive const box observable.box(1) autorun(() { console.log(box.get()) }) box.set(2)控制台依次打印1与2。源码原理显式代理映射box 的实现位于 annotations/box.ts与 ref 的关键差异在于代理的登记方式const proxy { set, get } ProxyRaw.set(proxy, store) RawProxy.set(store, proxy)ref 返回的是一个通过Object.defineProperty定义了value属性的普通proxy对象而 box 直接返回一个包含get/set方法的字面量proxy并通过ProxyRaw/RawProxy两个弱映射显式建立代理 ↔ 内部存储的双向关联。get()/set()内部同样执行依赖绑定bindTargetKeyWithCurrentReaction与变更广播runReactionsFromTargetKey并且set在值未变化时不会触发更新if (oldValue ! value)判断。当 box 作为注解使用时target存在它会用Object.defineProperty将代理对象作为不可写、不可配置的属性挂到目标上见 annotations/box.ts。作为注解使用给类属性标记响应式observable的各个形态除了直接调用还都实现了注解能力——即挂载MakeObModelSymbol标记见 internals.ts 的createAnnotation与getObservableMaker。在 TypeScript 装饰器场景下可以用它们标记类属性为响应式例如class Store { observable.ref count 0 observable.computed get double() { return this.count * 2 } }从源码看注解调用时createAnnotation会以{ target, key, value }访问器对象调用各实现target为类实例、key为属性名实现内部据此分支有target时通过Object.defineProperty定义属性并返回原对象无target时直接函数调用则创建独立容器返回详见 annotations/observable.ts 等。这就是一个 API 两种用法函数式创建 / 装饰器注解的统一入口设计。场景选型速查需求推荐 API理由管理嵌套的表单/业务对象状态observable/observable.deep深劫持任意层级读写都能触发更新对象层级深但只关心顶层替换observable.shallow减少代理开销避免不必要的深层包装由已有状态派生的值总和、过滤结果等observable.computed自动依赖追踪 惰性缓存性能最优包装数字、字符串等基础类型observable.ref属性式读写语法简洁偏好方法式读写、或需显式区分 APIobservable.boxget/set 语义明确便于封装小结observable是 formily/reactive 响应式体系的基石deep/shallow控制劫持深度computed提供依赖缓存与派生计算ref/box解决基础类型包装问题。理解它们各自的劫持粒度与底层代理/注解机制是正确使用 Formily 双向绑定、联动、校验等高级特性的前提。若想深入实践可以继续阅读 observable 的官方 API 文档并通过 observable.spec.ts 中的测试用例验证各种边界行为。赞分享前端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 observe API 全解析深度/浅度监听 Observable 对象的所有写操作formily/reactive observe API 全解析深度/浅度监听 Observable 对象的所有写操作 observe 是 formily前端UI组件Formily Reactive 的 raw API 详解如何从 observable 对象中取回源数据Formily Reactive 的 raw API 详解如何从 observable 对象中取回源数据 导读 raw 是 formily/reactive前端UI组件Formily Reactive 的 markRaw 详解让对象彻底脱离响应式劫持Formily Reactive 的 markRaw 详解让对象彻底脱离响应式劫持 导读 markRaw 是 formily/reactive https:前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表