
前端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。与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 / setget、iterate、has这些读取类操作虽然存在于OperationType联合类型中但不会触发observe的监听回调。从源码看observe的实现在 observe.ts其核心逻辑是向全局的ObserverListeners集合定义于 environment.ts注册一个监听器而 Observable 对象的写操作会通过 handlers.ts 中的 Proxy 处理器set、deleteProperty以及集合类型的instrumentationsadd、set、delete、clear分发到runReactionsFromTargetKey进而通知所有注册的 Observer 监听器。二、函数签名与类型定义observe的完整签名如下来自 observe.zh-CN.md 与 types.tstype PropertyKey string | number | symbol type ObservablePath Arraystring | 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可选变化回调每次监听到目标相关的操作时触发接收一个描述变化详情的IChangedeepbooleantrue是否深度监听。true时目标内部任意层级的嵌套对象/数组变化都会触发回调false时仅监听目标对象自身的直接操作返回值返回一个IDispose释放函数。调用dispose()后监听器会从全局ObserverListeners中移除后续操作不再触发回调。这一点在源码 observe.ts 中可以看到addListener返回的闭包会执行ObserverListeners.delete(listener)。类型合法性校验observe对入参类型有严格校验observe.tsif (target typeof target ! object) throw Error(Can not observe ${typeof target} type.)也就是说如果传入的是函数、字符串、数字等非对象类型会直接抛出Can not observe ... type错误。对应的测试用例见 observe.spec.tsexpect(() 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)注册监听器返回disposeobs.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 observableany({ 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.tsif (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.tsconst obs observableany({ 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.tsif ( 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 / symbolpathObservablePath从根到操作点的完整路径数组由node.path.concat(key)计算得出tree.tsobjectobject发生操作的目标对象对应IOperation.targetvalueany本次操作写入的新值oldValueany被覆盖前的旧值typeOperationType操作类型操作类型的实际取值虽然OperationType联合类型包含add/delete/clear/set/get/iterate/has七种但能被 observe 监听到的只有写操作。从 handlers.ts 的 Proxy 处理器与集合 instrumentations 中可以确认普通对象/数组set处理器在属性不存在时触发add已存在且值变化时触发sethandlers.tsdeleteProperty触发deletehandlers.tsMap/Setadd新增键、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节点通过RawNodeWeakMap或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是全局共享的ArraySetenvironment.ts因此在组件卸载、任务结束等场景下务必调用dispose避免监听器泄漏导致内存占用与无谓回调。十、与 autorun / reaction / Tracker 的选型建议维度observeautorun / 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项目地址https://gitcode.com/gh_mirrors/fo/formily点击查看免费下载相关推荐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),仅供参考