ARTICLE DETAIL

资讯详情

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

@formily/reactive model API 深度解析:用声明式语法自动构建响应式领域模型

@formily/reactive model API 深度解析:用声明式语法自动构建响应式领域模型 formily/reactive model API 深度解析用声明式语法自动构建响应式领域模型【免费下载链接】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/formilymodel是 formily/reactive 提供的领域模型Domain Model快速定义入口传入一个普通对象字面量即可自动完成 getter 的计算属性化、方法的 action 批量化以及普通属性的 observable 化无需手写任何注解。本文以 model.md 为骨架结合 model.ts、computed.ts、action.ts 等源码与测试用例系统讲解其用法、自动声明规则、底层实现原理及与define的配合方式。读完你将能熟练运用model定义带计算属性和批处理方法的响应式数据模型并理解autorun等响应式副作用为何会按预期精确触发。model 是什么一行代码完成领域模型的响应式声明在响应式编程中定义一个领域模型通常需要手动区分三类属性并分别标注响应式行为普通数据属性、由其他属性推导而来的计算属性getter/setter、以及会修改多个状态的业务方法。model的作用正是快速定义领域模型并自动声明模型的属性它将这一过程收敛为一次函数调用。其自动声明规则对应 model.md 的 Description为getter/setter 属性自动声明为 computed例如get cc() { return this.aa this.bb }读取时惰性求值、依赖变更时自动失效重算函数自动声明为 action例如update(aa, bb)方法体内部的多次赋值会被批量收集副作用只触发一次普通属性自动声明为 observable例如aa: 1读取可被追踪赋值可触发更新。也就是说你只需要按照领域模型的直觉去书写对象数据 计算属性 方法model负责补齐全部响应式语义这正是它与直接调用observable()的关键区别observable()只做深劫持代理见 observable.ts而model会进一步按属性类型应用差异化注解。函数签名与返回值原文档给出的签名如下interface modelTarget extends object { (target: Target): Target }入参target一个普通对象字面量或类实例其中可以包含普通属性、getter/setter 与方法返回值传入的target本身类型保持不变。从源码 model.ts 看model内部构造注解表后调用define(target, annotations)并原样返回target——它原地修改并增强传入对象而非返回一个克隆体。这也意味着调用后原对象引用即具备响应式能力。快速上手完整示例与执行过程推演原文档给出了一个完整的可运行示例见 model.md 的 Example 部分import { model, autorun } from formily/reactive const obs model({ aa: 1, bb: 2, get cc() { return this.aa this.bb }, update(aa, bb) { this.aa aa this.bb bb }, }) autorun(() { console.log(obs.cc) }) obs.aa 3 obs.update(4, 6)其执行过程可以分步推演model({...})aa、bb是普通属性 → 自动声明为observable深劫持cc是 getter → 自动声明为computedupdate是函数 → 自动声明为actionautorun(() console.log(obs.cc))首次执行时读取obs.cccomputed 依赖aa、bb副作用被登记到三者之上控制台输出3obs.aa 3直接赋值触发aa的响应式更新computedcc被标记为脏并重算autorun被重新执行输出6obs.update(4, 6)update作为 action 执行内部连续修改aa与bb。由于 action 的批量语义这两次赋值被合并为一次依赖通知autorun再次执行输出10。该行为与 define.spec.ts 中define model测试用例的预期一致action()执行后obs.aa为2可以作为验证参考。自动声明规则是如何实现的model 源码解析model的全部逻辑集中在 model.tsexport function modelTarget extends object any(target: Target): Target { const annotations Object.keys(target || {}).reduce((buf, key) { const descriptor Object.getOwnPropertyDescriptor(target, key) if (descriptor descriptor.get) { buf[key] observable.computed } else if (isFn(target[key])) { buf[key] action } else { buf[key] observable } return buf }, {}) return define(target, annotations) }三个判定分支与文档描述一一对应getter 判定通过Object.getOwnPropertyDescriptor(target, key)检测descriptor.get是否存在。存在即说明该属性是访问器属性标记为observable.computed。注意这里同时覆盖了仅定义get和同时定义get/set的情况——setter 由 computed 注解内部处理见下文函数判定isFn(target[key])为真则标记为action兜底其余普通属性统一标记为observable。随后model将所有注解交给define落地。definemodel.ts的执行链路是先对已观察对象或不可观察对象做短路返回isObservable/isSupportObservable检查对应测试用例 define.spec.ts 中传入数字、字符串、函数、数组时原样返回的行为然后为对象打上模型标记ObModelSymbol、建立数据树节点buildDataTree最后遍历注解表通过getObservableMaker(annotation)取到注解工厂并逐个应用。深入三种自动注解observable、computed 与 actionmodel自动选择的三类注解各自承担不同的响应式职责值得分别深入observable深劫持的可观察属性普通属性被标记为observable对应注解实现在 annotations/observable.ts它通过createObservable(target, key, value)递归地把属性值包括嵌套对象转换为 Proxy 代理实现深度劫持——读取任意深度的子属性都能被追踪修改任意深度的值都能触发依赖更新。这也是define.spec.ts中observable annotation用例define.spec.ts能对target.aa.bb.cc这种嵌套路径精确响应的原因。computed惰性求值与依赖缓存getter 被标记为observable.computed其实现细节在 annotations/computed.ts 中非常典型惰性求值内部维护store.value缓存与reaction._dirty脏标记get()时仅在_dirty true时才真正执行计算函数reaction()→compute()否则直接返回缓存值依赖追踪计算执行期间会把自身reaction推入ReactionStack从而收集其依赖的 observable 属性自动失效依赖变更时由_scheduler将_dirty置为true并触发依赖该 computed 的副作用runReactionsFromTargetKey实现依赖变 → 计算脏 → 读取时重算的闭环setter 支持若 getter 对象同时定义了setcomputed 注解会通过descriptor.set?.call(context, value)在batchStart/batchEnd包裹下执行computed.ts。define.spec.ts 的computed annotation用例验证了这一点autorun中首次读取target.cc时计算函数只执行 1 次修改aa后再读取才执行第 2 次中间多次读取不会重复计算。action批量更新与依赖收集隔离方法被标记为action实现见 action.tsexport const action: IAction createBoundaryAnnotation( () { batchStart() untrackStart() }, () { untrackEnd() batchEnd() } )它借助createBoundaryAnnotationinternals.ts把原方法替换为绑定到目标对象上的边界函数batchStart/batchEnd方法体内的所有赋值被收集到同一批次结束时统一派发依赖通知。这正是 define.spec.ts 的action annotation用例中setData同时修改aa.bb与aa.cc两个字段、autorun却只多触发一次的原因untrackStart/untrackEnd执行期间暂时关闭依赖收集避免 action 内部读取操作污染外部追踪上下文this 绑定createBoundaryFunction的bound方法将原函数以target为context重新包装因此即使把方法从对象上解构出来如测试中的const { action } obs; action()也能正确读写目标对象的属性。从自动到手动model 与 define 的关系model本质是按类型约定自动生成注解表并调用define的语法糖。当自动推断不满足需求时可以直接使用define手动指定每个属性的响应式行为签名见 define.mdinterface defineTarget extends object { ( target: Target, annotations?: { [key: string]: (...args: any[]) any } ): Target }define目前支持的全部注解同样记录于 define.md 的 Annotations 部分为observable/observable.deep深劫持响应式属性model对普通属性默认选用此项observable.boxget/set 容器通过.get()/.set()读写observable.computed计算属性model对 getter 默认选用observable.ref引用劫持仅劫持属性本身的替换不深入内部observable.shallow浅劫持仅首层响应式action/batch批量处理方法model对方法默认选用action。这些注解均挂在 observable.ts 导出的observable命名空间下使用方式可参考 define.md 的DomainModel类示例。典型取舍是需要全部默认语义、追求简洁时用model需要精细控制例如浅劫持、box/ref 容器、对方法使用batch而非action时用define。适用场景与注意事项首选默认语义凡是数据 计算属性 修改方法结构的领域模型用model一行即可获得完整响应式能力配合autorun、observe、reaction等 API 使用见 packages/reactive/docs/api 目录下的对应文档原地增强model返回的是传入对象本身并改写其属性描述符computed 通过Object.defineProperty重定义 getter/setter不要期望它返回新对象对象类型限制model/define只处理可观察的普通对象对数字、字符串、函数等非目标类型会原样返回由 model.ts 的守卫逻辑保证action 的副作用语义由于 action 默认关闭依赖收集untrack并批量派发在 action 内读取其他 observable 不会建立依赖关系——这正是方法内多次赋值只触发一次更新的实现前提。综上model是 formily/reactive 中声明式定义响应式模型的入口理解它的三条自动规则与背后的 computed/action/observable 注解实现能帮助你在使用 Formily 生态表单模型、字段模型等大量依赖此模式的场景时更准确地预期响应式行为并能在需要时无缝切换到define做更细粒度的控制。【免费下载链接】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),仅供参考
返回列表