- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
observe是@formily/reactive中用于监听 Observable 对象操作的 API。与autorun、reaction、Tracker的"依赖追踪式"响应不同,observe会直接监听 observable 对象上发生的所有操作(新增、删除、清空、赋值等),并支持深度监听与浅监听两种模式,非常适合实现日志记录、状态同步、持久化等场景。读完本文,你将掌握observe的完整签名、监听回调中的变化详情结构、深度/浅监听的行为差异、dispose释放机制,以及其背后的数据树(DataNode)实现原理。
一、observe 是什么:与 autorun/reaction/Tracker 的本质区别
在@formily/reactive中,autorun、reaction、Tracker都是"订阅式"响应 API:它们会追踪执行过程中被读取的依赖,当依赖发生变化时重新执行。这种机制建立在"读取追踪"之上。
observe则完全不同——它监听的是 observable 对象上的所有操作事件,而非依赖关系。根据官方文档的描述,使用observe会监听 observable 对象的所有操作,支持深度监听也支持浅监听。
需要特别强调的是文档中的警告:
注意:读取操作是不会被监听到的。
也就是说,observe只关心"写"一类操作(add / delete / clear / set),get、iterate、has这些读取类操作虽然存在于OperationType联合类型中,但不会触发observe的监听回调。
从源码看,observe的实现在 observe.ts,其核心逻辑是向全局的ObserverListeners集合(定义于 environment.ts)注册一个监听器;而 Observable 对象的写操作会通过 handlers.ts 中的 Proxy 处理器(set、deleteProperty)以及集合类型的instrumentations(add、set、delete、clear)分发到runReactionsFromTargetKey,进而通知所有注册的 Observer 监听器。
二、函数签名与类型定义
observe的完整签名如下(来自 observe.zh-CN.md 与 types.ts):
type PropertyKey = string | number | symbol type ObservablePath = Array<string | number> type OperationType = | 'add' | 'delete' | 'clear' | 'set' | 'get' | 'iterate' | 'has' interface IChange { key?: PropertyKey path?: ObservablePath object?: object value?: any oldValue?: any type?: OperationType } interface IDispose { (): void } interface observe { ( target: object, observer?: (change: IChange) => void, deep?: boolean //默认为true ): IDispose //释放监听 }参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
target | object | 必传 | 要监听的 observable 对象(也可以是其嵌套属性对应的 observable 子对象) |
observer | (change: IChange) => void | 可选 | 变化回调,每次监听到目标相关的操作时触发,接收一个描述变化详情的IChange |
deep | boolean | true | 是否深度监听。true时目标内部任意层级的嵌套对象/数组变化都会触发回调;false时仅监听目标对象自身的直接操作 |
返回值
返回一个IDispose释放函数。调用dispose()后,监听器会从全局ObserverListeners中移除,后续操作不再触发回调。这一点在源码 observe.ts 中可以看到:addListener返回的闭包会执行ObserverListeners.delete(listener)。
类型合法性校验
observe对入参类型有严格校验(observe.ts):
if (target && typeof target !== 'object') throw Error(`Can not observe ${typeof target} type.`)也就是说,如果传入的是函数、字符串、数字等非对象类型,会直接抛出Can not observe ... type错误。对应的测试用例见 observe.spec.ts:expect(() => observe(function () {})).toThrowError()。
三、基本用法:监听赋值操作
文档给出的最小用例(可直接复制运行):
import { observable, observe } from '@formily/reactive' const obs = observable({ aa: 11, }) const dispose = observe(obs, (change) => { console.log(change) }) obs.aa = 22 dispose()执行流程分析:
observable({ aa: 11 })创建了一个响应式代理对象;observe(obs, cb)注册监听器,返回dispose;obs.aa = 22触发 Proxy 的set处理器,产生一次set类型操作,回调被调用,change大致为{ key: 'aa', path: ['aa'], object: obs, value: 22, oldValue: 11, type: 'set' };dispose()移除监听器。
值得注意的是,observe在内部会先对target调用getRaw获取其原始对象,再通过getDataNode取得对应的DataNode(数据树节点),监听器注册的条件是"存在数据节点且observer是函数"(observe.ts)。因此被监听的目标必须是observable创建或已挂载到 observable 树中的对象,普通原生对象无法被监听。
四、深度监听(deep: true)
默认情况下deep为true,监听器会覆盖目标对象内部任意层级的嵌套变化。测试用例 observe.spec.ts 展示了完整行为:
const obs = observable<any>({ aa: { bb: { cc: [11, 22, 33], }, }, ee: observable([]), }) const handler = jest.fn() observe(obs, handler) obs.dd = 123 // 触发 1 次(目标自身新增属性 add) obs.aa.bb.cc.push(44) // 触发 1 次(深层数组 push 操作) delete obs.aa // 触发 1 次(删除属性 delete)在深度监听模式下,无论操作发生在多深的嵌套层级,只要该节点属于被监听目标的数据树(通过DataNode.contains判断),都会触发回调。从源码看,深度监听的判定逻辑为(observe.ts):
if (deep) { if (node.contains(targetNode)) { observer(new DataChange(operation, targetNode)) return } }其中node.contains(targetNode)会沿着targetNode的parent链向上回溯,逐一与被监听节点比对是否相等(实现见 tree.ts)。
关于已挂载子节点的特殊情况
同一个测试还揭示了一个重要边界:如果目标对象中嵌套的是预先创建好的独立 observable 对象(如ee: observable([])),那么直接操作该子对象内部(obs.ee.push(11))在第一次并不会触发监听;只有先对目标属性重新赋值(obs.ee = [])让该子对象重新"挂载"进数据树,之后的内部操作(obs.ee.push(11))才会被监听到。测试中对此也留了注释 "Are these expected behaviors?"。这是因为数据树节点是在"访问/赋值"时按需构建的,预先存在的独立 observable 子对象在首次赋值前尚未建立与父节点之间的树关系。建议:若要深度监听嵌套结构,尽量让嵌套数据以普通对象/数组形式挂在 observable 对象下,由响应式系统自动完成转换与挂载。
五、浅监听(deep: false)
将第三个参数设为false即可进行浅监听,此时只有目标对象自身的直接操作会触发回调,深层嵌套的变化不会。测试 observe.spec.ts:
const obs = observable<any>({ aa: { bb: { cc: [11, 22, 33], }, }, }) const handler = jest.fn() observe(obs, handler, false) obs.dd = 123 // 触发 1 次(目标自身 add) obs.aa.bb.cc.push(44) // 不触发(深层操作被过滤) delete obs.aa // 触发 1 次(目标自身 delete)从源码看,浅监听走的是另一条判定分支(observe.ts):
if ( node === targetNode || (node.targetRaw === targetRaw && node.key === operation.key) ) { observer(new DataChange(operation, targetNode)) }即只有"操作节点就是被监听节点本身",或"操作发生在被监听节点的原始对象上且 key 一致"时才会回调。
浅监听下对子节点赋值的行为
测试 observe.spec.ts 展示了另一种有意思的情形:对obs.aa这个子对象执行observe(obs.aa, handler, false)进行浅监听,然后反复整体替换obs.aa = { mm: 222 },每次替换都会触发回调(共 4 次)。这说明"替换整个子对象"这一操作本身会被子对象节点捕获(因为node.targetRaw === targetRaw && node.key === operation.key中,被替换对象的原始 target 正是obs自身,key 为aa)。而监听根对象obs时,obs.kk = 111这类无关操作不会触发对obs.aa的监听(observe.spec.ts 中 handler 调用次数为 0)。
六、监听回调中的变化详情(IChange / DataChange)
回调接收的change对象在类型上定义为IChange,实际运行时由 tree.ts 中的DataChange类构造。各字段含义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
key | PropertyKey | 发生操作的属性名(string / number / symbol) |
path | ObservablePath | 从根到操作点的完整路径数组,由node.path.concat(key)计算得出(tree.ts) |
object | object | 发生操作的目标对象(对应IOperation.target) |
value | any | 本次操作写入的新值 |
oldValue | any | 被覆盖前的旧值 |
type | OperationType | 操作类型 |
操作类型的实际取值
虽然OperationType联合类型包含add/delete/clear/set/get/iterate/has七种,但能被 observe 监听到的只有写操作。从 handlers.ts 的 Proxy 处理器与集合 instrumentations 中可以确认:
- 普通对象/数组:
set处理器在属性不存在时触发add,已存在且值变化时触发set(handlers.ts);deleteProperty触发delete(handlers.ts); - Map/Set:
add新增键、set覆盖键、delete删除键、clear清空(handlers.ts)。
也就是说,实际回调中type通常为add、set、delete、clear四种之一。
在回调中结合 path 与 type 过滤
测试 observe.spec.ts 给出了一个实用模式——只关心数组中元素的value字段变化,并利用path.join('.')得到可读路径:
const array = observable([{ value: 1 }, { value: 2 }]) const fn = jest.fn() const dispose = observe(array, (change) => { if (change.type === 'set' && change.key === 'value') { fn(change.path?.join('.')) } }) array[0].value = 3 // fn 收到的路径为 '0.value' array.splice(0, 1) array[0].value = 3 // 数组删除后,路径依然为 '0.value' dispose()这个用例还验证了splice删除元素后,响应式系统会正确维护数组索引对应的数据节点,新元素仍然能从0.value被监听到。
七、根节点整体替换(Root Replace)行为
测试 observe.spec.ts 覆盖了"同时监听根对象与子对象,然后整体替换子对象"的场景:
observe(obs, handler1) // 深度监听根对象 observe(obs.aa, handler) // 监听子对象 obs.aa obs.aa = { mm: 123 } // handler1 调用 1 次(根对象收到 set 操作) // handler 调用 1 次(子对象节点也捕获到替换) obs.aa = { bb: { cc: [11, 22, 33] } } obs.aa.bb.cc.push(44) // handler1 调用增至 3 次,handler 也增至 3 次结论:当整体替换某个被监听的子对象时,新旧监听器(根对象与子对象)都会收到通知;替换完成后新对象内部的操作会同时沿两条监听路径上报。这说明 observe 的监听是"节点 + 路径"双重绑定的,替换操作本身作为一次set操作既命中子节点(key 匹配),又命中根节点(contains 匹配)。
八、动态构建的数据树:observe 的底层原理
observe之所以能同时支持深度与浅度监听,得益于@formily/reactive内部维护的DataNode 数据树。
- 每个 observable 原始对象都关联一个
DataNode,节点记录target(父对象)、key(挂载属性)、value(自身值),并通过parent链向上连接(tree.ts); - 节点通过
RawNode(WeakMap)或ObModelNodeSymbol与原始对象建立映射(environment.ts、tree.ts); - 当嵌套对象被访问/赋值时,
buildDataTree会为它构建节点并挂到父节点下(tree.ts),数据树是按需动态构建的——这也解释了上文"预先创建的独立 observable 子对象需要先重新赋值才会被深度监听"的现象; DataChange.path通过node.path.concat(key)递归拼接父级路径得到,保证任何深度的操作都能给出从根到叶的完整路径。
监听器注册后,只要 observable 上发生任何写操作,就会广播给ObserverListeners中的全部监听器,再由每个监听器根据deep参数决定是否命中(contains或isEqual判定),最终包装成DataChange回调给用户。
九、释放监听:dispose 的正确用法
observe返回的dispose函数用于释放监听。测试 observe.spec.ts 验证了释放前后行为:
const dispose = observe(obs, handler) obs.kk = 123 // handler 调用 1 次 dispose() obs.aa = 123 // handler 不再被调用(仍为 1 次)实现上,dispose本质是执行ObserverListeners.delete(listener)(observe.ts),将监听器从全局集合中移除。注意:ObserverListeners是全局共享的ArraySet(environment.ts),因此在组件卸载、任务结束等场景下务必调用dispose,避免监听器泄漏导致内存占用与无谓回调。
十、与 autorun / reaction / Tracker 的选型建议
| 维度 | observe | autorun / reaction / Tracker |
|---|---|---|
| 监听机制 | 监听对象上的所有写操作事件 | 追踪函数执行中读取的依赖 |
| 关注点 | "谁被改了"(操作本身) | "哪些依赖变了导致重新执行" |
| 读取操作 | 不监听 | 是依赖追踪的基础 |
| 深度控制 | 通过deep参数显式控制 | 由依赖读取路径自然决定 |
| 典型场景 | 日志、审计、同步、持久化、调试 | UI 渲染、派生计算、副作用编排 |
一句话概括:如果关心"数据被如何修改"(操作事件流),用observe;如果关心"数据变化后要重新执行什么"(响应式计算),用autorun/reaction/Tracker。
十一、注意事项总结
- 读取不会被监听:
get、iterate、has出现在类型定义中,但不会触发 observe 回调; - 目标必须是 observable 对象:普通对象或未挂载到 observable 树中的对象无法被监听,非对象类型会直接抛错;
- 深度监听默认开启:
deep默认为true,如需浅监听显式传false; - 独立 observable 子对象需要先"挂载":预先创建的子 observable 对象在首次整体赋值前,其内部操作不会被深度监听;
- 务必调用 dispose:监听器注册在全局集合中,用完释放是防止泄漏的关键;
- 整体替换会双路触发:同时监听根对象与其子对象时,替换子对象会同时通知两条监听路径,设计逻辑时需留意重复回调。
通过本文的签名解析、源码走读与测试验证,你可以准确掌握observe的行为边界,并在日志审计、状态同步、跨端数据持久化等场景中放心使用它。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
@formily/reactive observe API 详解:操作级监听、深/浅监听与变更事件模型
@formily/reactive observe API 详解:操作级监听、深/浅监听与变更事件模型 导读 observe 是 @formily/reacti
前端UI组件Formily Reactive 的 raw API 详解:如何从 observable 对象中取回源数据
Formily Reactive 的 raw API 详解:如何从 observable 对象中取回源数据 导读 raw 是 @formily/reactive
前端UI组件解密冰川的脉搏:Open Global Glacier Model 如何重塑我们对冰河的理解
解密冰川的脉搏:Open Global Glacier Model 如何重塑我们对冰河的理解 在气候变化的时代,冰川如同地球的体温计,记录着全球变暖的微妙变化。
科研科学计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考