- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
model()是 @formily/reactive 提供的一键式领域模型定义入口:它基于属性的类型自动为对象成员声明合适的响应式行为——getter 属性自动变为 computed(计算属性)、函数自动变为 action(批处理动作)、普通属性自动变为 observable(深度响应式数据),从而让开发者用最少的样板代码构建出具备完整响应式能力的领域模型。本文以 packages/reactive/docs/api/model.zh-CN.md 为主线,结合 model.ts 等源码实现,深入讲解其自动标注规则、底层调用链、define()手动标注的扩展方式,以及它与 autorun/observe 的组合实践。
一、model 是什么:一句话概括
model是 @formily/reactive 包中用于快速定义领域模型(Domain Model)的工厂函数。与手动逐属性配置响应式行为的define不同,model会自动扫描传入对象的每个自有属性,并按以下规则"自动声明"响应式行为:
| 属性形态 | 自动声明的响应式类型 | 说明 |
|---|---|---|
getter 属性(带get描述符) | observable.computed | 计算属性,依赖变化时惰性重算 |
| 函数(方法) | action | 批处理动作,内部多次修改只触发一次通知 |
| 普通属性 | observable | 深度劫持的响应式数据 |
其函数签名(见 model.zh-CN.md)非常简洁:
interface model<Target extends object> { (target: Target): Target }输入一个普通对象,返回同一个对象(原地改造,返回引用不变),改造后该对象便具有完整的响应式追踪能力,可被autorun、observe、tracker等机制订阅。
二、从用例看 model 的核心能力
文档给出的最小用例展示了model的完整工作流:
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) // 首次输出 3 }) obs.aa = 3 // 直接改普通属性,触发 cc 重算,输出 5 obs.update(4, 6) // 经 action 批量修改 aa/bb,输出 10这里值得注意的几点实战细节:
- getter
cc被自动声明为 computed:cc在autorun中被读取,其依赖(aa、bb)一旦变化,cc会惰性重算并触发依赖它的autorun回调。因为cc是计算属性,update内部连续修改aa和bb时,cc只会在最终值确定后重算一次,避免了中间态的重复执行。 - 函数
update被自动声明为 action:action 内部的所有响应式修改处于同一个批处理边界内,autorun只会收到一次通知,而不是aa、bb各触发一次。这正是 define.spec.ts 中define model用例所验证的行为:action()调用一次后obs.aa变为 2,且整个过程中只发生一次响应式派发。 - 普通属性
aa、bb被自动声明为深度 observable:嵌套子对象同样会被递归劫持,后续新增、修改深层属性都能被追踪。
三、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) }整个流程分为两步:
3.1 第一步:遍历属性,生成 annotation 映射表
model通过Object.keys遍历对象的自有可枚举属性,并用Object.getOwnPropertyDescriptor检查每个属性的描述符,从而区分三种形态:
- 存在
get描述符 → 判定为计算属性,对应observable.computed; - 属性值类型为函数(
isFn判定,见 checkers.ts)→ 判定为方法,对应action; - 其余情况 → 判定为普通数据,对应
observable(即深度劫持)。
这一步产出一个{ [key]: annotation }形式的标注映射表,随后整体交给define执行。
3.2 第二步:调用 define 落地标注
define是model的底层执行者(同一个文件中的define函数,独立的 define.zh-CN.md 文档也对其做了说明):
export function define<Target extends object = any>( target: Target, annotations?: Annotations<Target> ): Target { if (isObservable(target)) return target // 已是响应式对象则直接返回 if (!isSupportObservable(target)) return target // 不支持的裸对象原样返回 target[ObModelSymbol] = target // 打上领域模型标记 buildDataTree(undefined, undefined, target) // 建立数据节点,便于路径追踪 for (const key in annotations) { const annotation = annotations[key] if (isAnnotation(annotation)) { getObservableMaker(annotation)({ target, key }) // 按标注执行对应 maker } } return target }define的关键逻辑:
- 幂等保护:
isObservable(见 externals.ts)检查对象是否已被响应化,已响应或不受支持的对象(如 Moment 对象、带toJS/toJSON的对象,见isSupportObservable的黑名单逻辑)会原样返回,不会重复包装; - 模型标记:通过
ObModelSymbol(定义于 environment.ts)标记对象为领域模型,后续raw()等 API 可以据此取回原始引用; - 数据树构建:
buildDataTree(见 tree.ts)为对象创建DataNode,这是contains、路径计算等功能的数据基础; - 标注执行:每个 annotation 都通过
getObservableMaker(见 internals.ts)解析出真正的实现函数,并以{ target, key }为访问者参数执行,完成属性级别的响应式改造。
3.3 三种 annotation 在底层的不同表现
observable(深度劫持):把属性值递归代理为 Proxy,嵌套的普通对象/数组/集合都会被继续劫持(对应 observable.ts 中observable.deep = annotations.observable的定义);observable.computed(计算属性):见 computed.ts,内部创建一个ComputedReaction,通过defineProperty重写目标属性的get/set:get时若依赖收集栈中有正在运行的 reaction,则绑定依赖;脏标记为true时才惰性重算,并把结果存入内部 store;set时(即用户通过 setter 反向赋值)自动包裹在batchStart/batchEnd中;action(批处理动作):见 action.ts,通过createBoundaryAnnotation在方法执行前后分别执行batchStart + untrackStart与untrackEnd + batchEnd,从而把方法内部的所有修改收敛为一次通知,且执行过程中不收集新依赖(untrack语义)。
四、精确控制:从 model 升级到 define
model是"约定优于配置"的快速通道,但它的自动判定规则是固定的:凡是函数一律视为 action、凡是 getter 一律视为 computed。当业务需要更精细的控制时,可以使用define手动为每个属性指定 annotation,甚至可以混用observable.shallow(浅劫持)、observable.box(get/set 容器)、observable.ref(引用劫持)和batch模式等方法(完整清单见 define.zh-CN.md):
import { define, observable, action, autorun } from '@formily/reactive' class DomainModel { deep = { aa: 1 } shallow = {} box = 0 ref = '' constructor() { define(this, { deep: observable, // 深度劫持 shallow: observable.shallow, // 仅劫持第一层 box: observable.box, // get/set 容器,box.get()/box.set() ref: observable.ref, // 引用级劫持 computed: observable.computed, action, }) } get computed() { return this.deep.aa + this.box.get() } action(aa, box) { this.deep.aa = aa this.box.set(box) } } const model = new DomainModel() autorun(() => { console.log(model.computed) }) model.action(1, 2) model.action(1, 2) // 重复调用且值未变化,不会重复触发响应 model.action(3, 4)define接受的 annotation 与行为对照(均可从 define.spec.ts 的用例中验证):
| Annotation | 行为 | 测试验证 |
|---|---|---|
observable/observable.deep | 深度劫持,嵌套对象递归代理 | observable annotation用例 |
observable.shallow | 仅劫持第一层,深层对象原样保留 | shallow annotation用例 |
observable.box | 提供get()/set()的容器 | box annotation用例 |
observable.ref | 只劫持属性引用本身 | ref annotation用例 |
observable.computed | 计算属性,依赖变化惰性重算 | computed annotation用例 |
action/batch | 方法体成为批处理边界 | action annotation用例 |
另外注意:define对非对象输入(数字、字符串、函数、数组)会原样返回不做处理,这一点在define.spec.ts的unexpect target用例中有明确覆盖(见 define.spec.ts)。
五、与 autorun / observe 组合的响应式闭环
model返回的对象可以直接接入 @formily/reactive 的所有订阅 API,形成"模型变更 → 视图/副作用更新"的闭环。核心导出均来自 index.ts,常用组合包括:
import { model, autorun, observe } from '@formily/reactive' const store = model({ count: 0, get double() { return this.count * 2 }, increment() { this.count++ }, }) // 1) autorun:任意依赖变化时重新执行 autorun(() => { console.log('count =', store.count, 'double =', store.double) }) // 2) observe:观察对象整体或某个 key 的变化 observe(store, (event) => { console.log('changed key:', event.key, 'oldValue:', event.oldValue, 'value:', event.value) }) store.increment() // 触发 autorun 与 observe 回调其中:
autorun是"立即执行一次 + 依赖变化重跑"的追踪器,适合驱动渲染或副作用;observe更接近事件监听,回调携带{ type, key, value, oldValue }等变更信息(DataChange结构见 tree.ts),适合做持久化、日志、联动等副作用;- 由于
increment是 action,count++只会触发一次通知,即使一次方法内修改多个字段也只会派发一次。
六、注意事项与边界
- 对象必须可响应化:
model内部会调用isSupportObservable(见 externals.ts)做兼容性检查。Moment 对象、JSON Schema 对象、带toJS/toJSON/$$typeof的库对象、已被markRaw标记的对象都不会被劫持,model会原样返回,避免破坏第三方库内部机制。 - 重复调用安全:对已是响应式对象再次调用
model/define会因isObservable检查直接返回,不会重复代理。 - getter 必须有 setter 才能反向赋值:
model自动声明的 computed 支持 setter(见 computed.ts),但只有原对象定义了set时才有效;未定义 setter 的只读 getter 只能作为派生值读取。 - this 绑定:action 在批处理执行时会用
bound包装方法(createBoundaryAnnotation中的boundary.bound(target[key], target),见 internals.ts),方法内的this会被绑定到模型对象本身,因此文档用例中的this.aa = aa可以安全使用。 - 包版本与适用环境:本文描述的行为基于仓库当前 @formily/reactive 2.3.7 版本的实现;该包支持 npm 直装(
npm install @formily/reactive),构建产物包含 CJS/ESM/UMD 多种格式,可同时用于浏览器与 Node 环境(对应package.json中的main/module/umd:main字段)。
七、小结
model用一行调用完成了"普通对象 → 响应式领域模型"的转换:getter 自动变 computed、函数自动变 action、普通属性自动变 observable,配合 autorun/observe 即可构建出高性能、可预测的响应式数据层。当自动规则无法满足需求时,define提供了逐属性手动的完整控制面。两者的核心实现(model.ts、define、computed.ts、action.ts)与测试用例(define.spec.ts)均可在此仓库中继续深入研读,是理解 Formily 整套响应式内核的最佳切入点。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
Formily Reactive 的 define 领域模型定义:深入解析注解式响应式建模
Formily Reactive 的 define 领域模型定义:深入解析注解式响应式建模 导读 define 是 @formily/reactive 包提供的
前端UI组件@formily/reactive model API 深度解析:用声明式语法自动构建响应式领域模型
@formily/reactive model API 深度解析:用声明式语法自动构建响应式领域模型 model 是 @formily/reactive htt
前端UI组件Formily 核心架构解析:基于 @formily/reactive 响应式领域模型的设计原理
Formily 核心架构解析:基于 @formily/reactive 响应式领域模型的设计原理 导读 本文围绕 packages/core/docs/guid
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考