@formily/reactive model API 深度解析:用声明式语法自动构建响应式领域模型
2026/9/23 12:53:25 网站建设 项目流程

@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/formily

model是 @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 model<Target 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)

其执行过程可以分步推演:

  1. model({...})aabb是普通属性 → 自动声明为observable(深劫持);cc是 getter → 自动声明为computedupdate是函数 → 自动声明为action
  2. autorun(() => console.log(obs.cc)):首次执行时读取obs.cc,computed 依赖aabb,副作用被登记到三者之上,控制台输出3
  3. obs.aa = 3:直接赋值触发aa的响应式更新,computedcc被标记为脏并重算,autorun被重新执行,输出6
  4. obs.update(4, 6)update作为 action 执行,内部连续修改aabb。由于 action 的批量语义,这两次赋值被合并为一次依赖通知,autorun再次执行,输出10

该行为与 define.spec.ts 中define model测试用例的预期一致(action()执行后obs.aa2),可以作为验证参考。

自动声明规则是如何实现的:model 源码解析

model的全部逻辑集中在 model.ts:

export function model<Target 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落地。define(model.ts)的执行链路是:先对已观察对象或不可观察对象做短路返回(isObservable/isSupportObservable检查,对应测试用例 define.spec.ts 中传入数字、字符串、函数、数组时原样返回的行为),然后为对象打上模型标记ObModelSymbol、建立数据树节点(buildDataTree),最后遍历注解表,通过getObservableMaker(annotation)取到注解工厂并逐个应用。

深入三种自动注解:observable、computed 与 action

model自动选择的三类注解各自承担不同的响应式职责,值得分别深入:

observable:深劫持的可观察属性

普通属性被标记为observable,对应注解实现在 annotations/observable.ts:它通过createObservable(target, key, value)递归地把属性值(包括嵌套对象)转换为 Proxy 代理,实现深度劫持——读取任意深度的子属性都能被追踪,修改任意深度的值都能触发依赖更新。这也是define.spec.tsobservable 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 对象同时定义了set,computed 注解会通过descriptor.set?.call(context, value)batchStart/batchEnd包裹下执行(computed.ts)。

define.spec.ts 的computed annotation用例验证了这一点:autorun中首次读取target.cc时计算函数只执行 1 次,修改aa后再读取才执行第 2 次,中间多次读取不会重复计算。

action:批量更新与依赖收集隔离

方法被标记为action,实现见 action.ts:

export const action: IAction = createBoundaryAnnotation( () => { batchStart() untrackStart() }, () => { untrackEnd() batchEnd() } )

它借助createBoundaryAnnotation(internals.ts)把原方法替换为绑定到目标对象上的边界函数:

  • batchStart/batchEnd:方法体内的所有赋值被收集到同一批次,结束时统一派发依赖通知。这正是 define.spec.ts 的action annotation用例中,setData同时修改aa.bbaa.cc两个字段、autorun却只多触发一次的原因;
  • untrackStart/untrackEnd:执行期间暂时关闭依赖收集,避免 action 内部读取操作污染外部追踪上下文;
  • this 绑定createBoundaryFunctionbound方法将原函数以targetcontext重新包装,因此即使把方法从对象上解构出来(如测试中的const { action } = obs; action())也能正确读写目标对象的属性。

从自动到手动:model 与 define 的关系

model本质是"按类型约定自动生成注解表并调用define"的语法糖。当自动推断不满足需求时,可以直接使用define手动指定每个属性的响应式行为,签名见 define.md:

interface define<Target extends object> { ( target: Target, annotations?: { [key: string]: (...args: any[]) => any } ): Target }

define目前支持的全部注解(同样记录于 define.md 的 Annotations 部分)为:

  • observable/observable.deep:深劫持响应式属性(model对普通属性默认选用此项);
  • observable.box:get/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一行即可获得完整响应式能力,配合autorunobservereaction等 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询