@formily/reactive 响应式内核
1. 核心机制图(可口述)
observable/define/model将对象代理化。autorun/reaction/Tracker在读取阶段收集依赖。- 写入阶段触发
runReactionsFromTargetKey调度。 batch/action/untracked控制触发边界与收敛行为。
2. 为什么用 Proxy(而不是 defineProperty)
从 handlers.ts 可见,Formily 响应式完整拦截了:
get/set/delete/ownKeys/has- Map/Set/WeakMap/WeakSet 的
get/set/add/delete/clear/iterate
这让它能支持:
- 普通对象与数组;
- 集合类型;
- 动态 key 与迭代依赖追踪。
这类能力在大规模动态表单里非常关键(字段集合与路径本身经常变化)。
3. 依赖收集与触发的关键实现点
3.1 收集:读时绑定
bindTargetKeyWithCurrentReaction:把当前 reaction 绑定到 target+key。ReactionStack维护当前追踪上下文。
3.2 触发:写时调度
runReactionsFromTargetKey统一处理 add/set/delete/clear。- 数组/集合结构变化会触发额外 iteration 依赖(如
length/ITERATION_KEY)。
3.3 去重与防抖动
PendingReactions/PendingScopeReactions去重批量执行。batchStart/batchEnd在批处理边界统一 flush。
4. computed 的实现特点(高频)
annotations/computed.ts 的设计重点:
- computed 本质是一个特殊 reaction(
_isComputed)。 - 采用
_dirty懒求值策略;依赖变更只标脏,不立即重算。 - 被其他 reaction 读取时,再触发求值与缓存更新。
收益:
- 对“频繁写、稀疏读”的场景非常友好;
- 避免在写时链式计算放大。
5. action / batch / untracked 的职责边界
batch:合并多次变更,减少重复调度。action:batch + untrack,避免写操作过程误收集依赖。untracked:临时关闭依赖收集,用于“只想执行,不想订阅”的逻辑。
在 Formily core 里,这三者被大量用于:
- 字段初始化;
- 批量
setFieldState; - 校验结果写入;
- schema 编译落状态。
6. React 场景下的资源释放问题
@formily/reactive-react 的 useCompatFactory 为了兼容 StrictMode/Concurrent 场景,额外引入了 GarbageCollector(FinalizationRegistry + fallback timer):
- 正常卸载时主动
dispose; - 极端场景下兜底回收,避免 reaction 泄漏。
这是“响应式系统 + React 运行时”结合时很实际的工程点。
7. 与 MobX 类方案的关系(常见说法)
Formily reactive 的能力接近轻量 MobX 核心子集,但针对 Formily 场景做了定向优化:
- 更强调低层可控(可直接与 core 内部调度衔接)。
- 重点优化路径化字段状态与批量更新。
- 通过
Tracker对 React observer 场景做了更薄桥接。
8. 延伸追问:链式 reaction 的边界
社区 PR 中有关于链式响应遗漏与多余触发的讨论与修复:
说明这套系统在“正确性 vs 性能”之间持续微调,属于响应式框架的典型演进轨迹。