@formily/reactive observe API 全解析:深度/浅度监听 Observable 对象的所有写操作
2026/9/24 21:41:56 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】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
点击查看免费下载

observe@formily/reactive中用于监听 Observable 对象操作的 API。与autorunreactionTracker的"依赖追踪式"响应不同,observe会直接监听 observable 对象上发生的所有操作(新增、删除、清空、赋值等),并支持深度监听与浅监听两种模式,非常适合实现日志记录、状态同步、持久化等场景。读完本文,你将掌握observe的完整签名、监听回调中的变化详情结构、深度/浅监听的行为差异、dispose释放机制,以及其背后的数据树(DataNode)实现原理。

一、observe 是什么:与 autorun/reaction/Tracker 的本质区别

@formily/reactive中,autorunreactionTracker都是"订阅式"响应 API:它们会追踪执行过程中被读取的依赖,当依赖发生变化时重新执行。这种机制建立在"读取追踪"之上。

observe则完全不同——它监听的是 observable 对象上的所有操作事件,而非依赖关系。根据官方文档的描述,使用observe会监听 observable 对象的所有操作,支持深度监听也支持浅监听。

需要特别强调的是文档中的警告:

注意:读取操作是不会被监听到的。

也就是说,observe只关心"写"一类操作(add / delete / clear / set),getiteratehas这些读取类操作虽然存在于OperationType联合类型中,但不会触发observe的监听回调。

从源码看,observe的实现在 observe.ts,其核心逻辑是向全局的ObserverListeners集合(定义于 environment.ts)注册一个监听器;而 Observable 对象的写操作会通过 handlers.ts 中的 Proxy 处理器(setdeleteProperty)以及集合类型的instrumentationsaddsetdeleteclear)分发到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 //释放监听 }

参数说明

参数类型默认值说明
targetobject必传要监听的 observable 对象(也可以是其嵌套属性对应的 observable 子对象)
observer(change: IChange) => void可选变化回调,每次监听到目标相关的操作时触发,接收一个描述变化详情的IChange
deepbooleantrue是否深度监听。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()

执行流程分析:

  1. observable({ aa: 11 })创建了一个响应式代理对象;
  2. observe(obs, cb)注册监听器,返回dispose
  3. obs.aa = 22触发 Proxy 的set处理器,产生一次set类型操作,回调被调用,change大致为{ key: 'aa', path: ['aa'], object: obs, value: 22, oldValue: 11, type: 'set' }
  4. dispose()移除监听器。

值得注意的是,observe在内部会先对target调用getRaw获取其原始对象,再通过getDataNode取得对应的DataNode(数据树节点),监听器注册的条件是"存在数据节点且observer是函数"(observe.ts)。因此被监听的目标必须是observable创建或已挂载到 observable 树中的对象,普通原生对象无法被监听。

四、深度监听(deep: true)

默认情况下deeptrue,监听器会覆盖目标对象内部任意层级的嵌套变化。测试用例 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)会沿着targetNodeparent链向上回溯,逐一与被监听节点比对是否相等(实现见 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类构造。各字段含义如下:

字段类型说明
keyPropertyKey发生操作的属性名(string / number / symbol)
pathObservablePath从根到操作点的完整路径数组,由node.path.concat(key)计算得出(tree.ts)
objectobject发生操作的目标对象(对应IOperation.target
valueany本次操作写入的新值
oldValueany被覆盖前的旧值
typeOperationType操作类型

操作类型的实际取值

虽然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通常为addsetdeleteclear四种之一。

在回调中结合 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参数决定是否命中(containsisEqual判定),最终包装成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 的选型建议

维度observeautorun / reaction / Tracker
监听机制监听对象上的所有写操作事件追踪函数执行中读取的依赖
关注点"谁被改了"(操作本身)"哪些依赖变了导致重新执行"
读取操作不监听是依赖追踪的基础
深度控制通过deep参数显式控制由依赖读取路径自然决定
典型场景日志、审计、同步、持久化、调试UI 渲染、派生计算、副作用编排

一句话概括:如果关心"数据被如何修改"(操作事件流),用observe;如果关心"数据变化后要重新执行什么"(响应式计算),用autorun/reaction/Tracker

十一、注意事项总结

  1. 读取不会被监听getiteratehas出现在类型定义中,但不会触发 observe 回调;
  2. 目标必须是 observable 对象:普通对象或未挂载到 observable 树中的对象无法被监听,非对象类型会直接抛错;
  3. 深度监听默认开启deep默认为true,如需浅监听显式传false
  4. 独立 observable 子对象需要先"挂载":预先创建的子 observable 对象在首次整体赋值前,其内部操作不会被深度监听;
  5. 务必调用 dispose:监听器注册在全局集合中,用完释放是防止泄漏的关键;
  6. 整体替换会双路触发:同时监听根对象与其子对象时,替换子对象会同时通知两条监听路径,设计逻辑时需留意重复回调。

通过本文的签名解析、源码走读与测试验证,你可以准确掌握observe的行为边界,并在日志审计、状态同步、跨端数据持久化等场景中放心使用它。

  • 前端
  • UI组件

【免费下载链接】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),仅供参考

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

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

立即咨询