Formily Reactive toJS 详解:从 observable 到普通 JS 对象的深度递归转换
前端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点击查看免费下载导读toJS是 Formily 响应式核心 formily/reactive 提供的重要工具函数用于将 observable 响应式对象深度递归地转换回普通 JS 对象。在实际开发中当我们完成表单数据收集、需要将响应式状态提交给后端、序列化到本地存储或传递给非响应式库如 axios、echarts、富文本编辑器时toJS就是打通响应式世界与普通数据世界的标准出口。读完本文你将掌握toJS的完整签名、底层递归实现原理、与markRaw的联动行为以及循环引用场景下的安全用法。一、toJS 是什么toJS是 formily/reactive 对外导出的核心 API 之一其官方描述为深度递归将 observable 对象转换成普通 JS 对象。也就是说它会把通过observable()创建的响应式代理Proxy还原为无响应式能力的原生数据。转换后的结果与原始 observable不再共享依赖追踪关系后续对普通对象的修改不会触发任何 autorun 响应。一个需要特别注意的行为是如果对一个已经是 observable 的对象标记markRaw那么toJS不会将它转换成普通对象。这一行为在 toJS.zh-CN.md 与 markRaw.zh-CN.md 中均有明确说明其根源在源码实现中也有体现详见本文第三节。二、签名与基本用例toJS的类型签名非常简洁interface toJST { (target: T): T }即输入什么类型的值就返回什么类型的值泛型T保证类型在转换前后保持不变。注意它只接受一个参数深度转换是默认行为无需额外配置。官方文档给出的基础用例见 toJS.zh-CN.mdimport { observable, autorun, toJS } from formily/reactive const obs observable({ aa: { bb: { cc: 123, }, }, }) const js toJS(obs) autorun(() { console.log(js.aa.bb.cc) //变化时不会触发 }) js.aa.bb.cc 321在这个例子中obs是一个嵌套三层的 observable 对象obs.aa.bb.cc的读取会产生依赖收集toJS(obs)得到的是普通对象js其内部不再存在 Proxy 代理在autorun中读取js.aa.bb.cc不会建立任何响应式依赖因此注释说明变化时不会触发直接修改js.aa.bb.cc 321由于js是普通对象该赋值既不会触发任何反应也不会反向影响obs。这正是toJS的典型用途生成一份脱离响应式系统的数据快照。三、源码级原理toJS 的深度递归实现toJS的真实实现位于 packages/reactive/src/externals.ts完整逻辑如下export const toJS T(values: T): T { const visited new WeakSetany() const _toJS: typeof toJS (values: any) { if (visited.has(values)) { return values } if (values values[RAW_TYPE]) return values if (isArr(values)) { if (isObservable(values)) { visited.add(values) const res: any [] values.forEach((item: any) { res.push(_toJS(item)) }) visited.delete(values) return res } } else if (isPlainObj(values)) { if (isObservable(values)) { visited.add(values) const res: any {} for (const key in values) { if (hasOwnProperty.call(values, key)) { res[key] _toJS(values[key]) } } visited.delete(values) return res } } return values } return _toJS(values) }结合源码可以梳理出以下几个关键机制1. 深度递归 循环引用保护_toJS对数组isArr和普通对象isPlainObj分别递归处理每个元素/自有属性。为了应对对象自引用如obj.obj obj导致的无限递归实现使用WeakSet记录访问过的对象递归前visited.add(values)递归完成后visited.delete(values)。WeakSet不会阻止对象被垃圾回收且只在递归路径上生效适合作为访问栈标记。这一点在测试 packages/reactive/src/tests/externals.spec.ts 中有直接验证test(recursive references tojs, () { const obj: any { aa: 111 } obj.obj obj const obs observableany(obj) obs.obs obs expect(toJS(obs)).toBeTruthy() const arrObs observable([{ aa: 1 }, { bb: 2 }, { cc: 3 }]) expect(toJS(arrObs)).toEqual([{ aa: 1 }, { bb: 2 }, { cc: 3 }]) })2. 递归出口非 observable 原样返回只有被isObservable判定的对象才进入递归分支其余值原始类型、未被代理的函数、类实例等一律原样返回。isObservable的定义位于同一文件export const isObservable (target: any) { return ProxyRaw.has(target) || !!target?.[ObModelSymbol] }即对象要么是ProxyRawProxy → raw 映射见 environment.ts中登记过的 Proxy要么是带ObModelSymbol的模型对象二者都算 observable。3. markRaw 的短路RAW_TYPE 标记toJS内部有一行非常关键的判断if (values values[RAW_TYPE]) return valuesRAW_TYPE是一个模块级私有 Symbolexternals.ts由markRaw写入。这就是文档警告对已 observable 对象标记 markRaw 后toJS 不会转换它的实现根源一旦对象带有RAW_TYPE标记toJS会直接把它原样返回不做任何深拷贝。四、与 markRaw 的联动理解 RAW_TYPE 的语义markRaw的官方语义是标记任意一个对象或者类原型为永远不可被 observable 劫持优先级比 markObservable 高见 markRaw.zh-CN.md。其实现同样在 externals.tsexport const markRaw T(target: T): T { if (!target) return if (isFn(target)) { target.prototype[RAW_TYPE] true } else { target[RAW_TYPE] true } return target }要点如下函数类级标记对构造函数调用markRaw(Class)会在Class.prototype上写入RAW_TYPE从而影响该类的所有实例实例级标记对实例调用markRaw(instance)只影响当前实例与 toJS 的关系无论对象是否已被 observable 化只要带有RAW_TYPE标记toJS都将其视为应当原样透传的数据不做转换。这与markRaw 优先级最高的语义一致——被标记为 raw 的数据在toJS中同样保持原样。配套测试 externals.spec.ts 从多个角度验证了标记组合的优先级markRaw({ aa: 111 })后再observable()结果不可观察isObservable为 falsemarkRaw(Class)后new Class()的实例均不可观察markRaw(markObservable({...}))与markObservable(markRaw({...}))最终都不可观察证明markRaw 优先级高于 markObservable。五、isSupportObservable谁会被排除在 observable 之外toJS只转换 observable 数据而一个对象能否成为 observable由isSupportObservable同文件 externals.ts决定。该函数对普通对象有一系列排除规则if (target[RAW_TYPE]) return false if (target[OBSERVABLE_TYPE]) return true if ($$typeof in target _owner in target) return false // React 元素 if (target[_isAMomentObject]) return false // moment 对象 if (target[_isJSONSchemaObject]) return false // JSON Schema 对象 if (isFn(target[toJS])) return false // 自带 toJS if (isFn(target[toJSON])) return false // 自带 toJSON return true这些规则同样有测试覆盖externals.spec.tsReact 元素带$$typeof与_owner、moment 对象、JSON Schema 对象会被自动排除避免被代理破坏内部状态自带toJS/toJSON方法的对象不会被代理从而保留自定义序列化能力如 moment 的toJSONMap / WeakMap / Set / WeakSet 始终支持 observablenull、undefined等无效值不支持。理解这一点有助于避免为什么toJS对这个对象没生效的困惑如果对象本身根本没有被 observable 化toJS自然原样返回它。六、实用场景与注意事项1. 典型使用场景提交数据将表单响应式状态toJS化后交给 axios/fetch 发送避免 Proxy 对象在序列化时出现意外行为持久化把状态快照写入 localStorage/IndexedDB 或传给 postMessage保证存储的是纯数据对接外部库echarts、canvas、富文本等库通常不需要响应式传入普通对象更安全高效调试在控制台打印toJS(state)可以看到清晰可读的普通对象结构。2. 注意事项快照是一次性的toJS返回的是全新拷贝数组/对象均为新引用与源 observable 完全解耦修改互不影响markRaw 对象不会被转换被markRaw标记过的对象在toJS中原样透传这是文档明确声明并受源码保障的行为非 observable 原样返回如果传入的本来就是普通对象、函数或原始类型toJS不做任何处理类型保持签名(target: T): T保证转换前后 TypeScript 类型不变可直接用于类型安全的数据出口不推荐反向滥用频繁toJS会创建大量副本若需要拿到底层源数据而非拷贝官方提供rawAPI见 raw.zh-CN.md但官方同样注明通常情况下并不推荐使用。七、小结能力说明功能深度递归将 observable 转为普通 JS 对象签名toJST(target: T): T递归支持数组、普通对象深度遍历WeakSet防循环引用markRaw 联动带RAW_TYPE标记的对象原样返回不转换递归出口非 observable 值原样返回源码位置packages/reactive/src/externals.ts测试验证packages/reactive/src/tests/externals.spec.tstoJS虽小却是响应式数据走出 formily/reactive 世界的必经之路。理解它的递归策略、RAW_TYPE短路逻辑以及与isSupportObservable的配合能帮助你在表单提交、状态持久化和第三方库对接等场景中写出更稳妥的代码。赞分享前端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 响应式核心 API 深度解析toJS 如何把 observable 对象还原为普通 JS 对象Formily 响应式核心 API 深度解析toJS 如何把 observable 对象还原为普通 JS 对象 toJS 是 formily/reactiv前端UI组件Formily Reactive 的 raw API 详解如何从 observable 对象中取回源数据Formily Reactive 的 raw API 详解如何从 observable 对象中取回源数据 导读 raw 是 formily/reactive前端UI组件formily/reactive observe API 全解析深度/浅度监听 Observable 对象的所有写操作formily/reactive observe API 全解析深度/浅度监听 Observable 对象的所有写操作 observe 是 formily前端UI组件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Kubernetes 边缘节点高可用配置实战:keepalived VIP + Traefik Ingress 单一入口方案(kubernetes-handbook)

Kubernetes 边缘节点高可用配置实战:keepalived VIP + Traefik Ingress 单一入口方案(kubernetes-handbook)

教程云原生容器编排 【免费下载链接】kubernetes-handbook Kubernetes 架构与生态:从云原生到 AI 原生基础设施的构建指南 项目地址: https://gitcode.com/gh_mirrors/ku/kubernetes-handbook 点击查看 免费下载 导读 在 Kubernetes 集群中&#xff0c…

2026/9/23 22:27:36 阅读更多 →
Python二维码与条形码生成识别实战:从批量制作到摄像头实时扫码

Python二维码与条形码生成识别实战:从批量制作到摄像头实时扫码

最近做的一个小工具涉及条形码和二维码的生成与识别,前后折腾了几天,总算把从生成、打印、到扫码入库这套流程完整跑通了。整个过程踩了不少坑,比如pyzbar在Windows上安装失败、批量生成标签时编码乱码、摄像头实时识别时二维码一闪而过却识别…

2026/9/23 22:27:36 阅读更多 →
SpringBoot+Vue全栈宠物业务系统开发实践

SpringBoot+Vue全栈宠物业务系统开发实践

1. 项目概述"134遇见宠爱"宠物业务系统是一个基于SpringBootVue微信小程序的全栈项目,专为宠物服务行业设计。作为一名有5年全栈开发经验的工程师,我在实际开发中发现传统宠物店管理系统往往存在几个痛点:前后端耦合严重导致迭代困…

2026/9/23 22:26:35 阅读更多 →

最新新闻

OCR识别性能评估全指南:从指标计算到多引擎选型实操

OCR识别性能评估全指南:从指标计算到多引擎选型实操

1. OCR算法识别性能评估的核心框架与选型逻辑OCR识别性能评估这件事,表面上看就是拿几张图跑一跑,看识别结果对不对。但真正做过完整评估的人都知道,这里面的坑远比想象中多。我前后参与过三轮OCR引擎的选型评估,从早期用Tesserac…

2026/9/23 23:12:37 阅读更多 →
基于Jupyter Notebook的Python用户画像构建:RFM实战指南

基于Jupyter Notebook的Python用户画像构建:RFM实战指南

简介:这套基于Jupyter Notebook的Python用户画像构建源码,面向希望系统性学习用户画像的数据分析师、产品运营及Python开发者,可帮助读者从原始用户行为数据出发,完成多维度画像标签的快速构建。资源包共20个文件,含13…

2026/9/23 23:12:37 阅读更多 →
K线周期规则实战:大周期定方向,小周期找买卖点

K线周期规则实战:大周期定方向,小周期找买卖点

1. 周期规则的本质:先搞清楚K线背后的时间级别做交易时间久了你会发现一个很扎心的事实:绝大多数人亏钱,不是不懂技术指标,而是把不同级别的信号混在一起用。日线刚出现买入信号,15分钟图一跌就拿不住,反过…

2026/9/23 23:12:37 阅读更多 →
区块链数据共享系统源码解析:IPFS存储+以太坊记账+ABE授权

区块链数据共享系统源码解析:IPFS存储+以太坊记账+ABE授权

简介:这套基于IPFS、Ethereum与基于属性加密(ABE)的区块链安全数据共享系统设计源码,面向区块链开发者和数据安全研究人员,适用于金融、医疗、供应链等对访问控制要求较高的场景,通过IPFS实现分布式存储&am…

2026/9/23 23:12:37 阅读更多 →
kornia YUV 色彩转换:docstring 示例修复、测试覆盖恢复与形状校验深度解析

kornia YUV 色彩转换:docstring 示例修复、测试覆盖恢复与形状校验深度解析

计算机视觉深度学习人工智能图像处理 【免费下载链接】kornia 🐍 空间人工智能的几何计算机视觉库 项目地址: https://gitcode.com/kornia/kornia 点击查看 免费下载 kornia 在 kornia.color 模块中提供了一套完整的 YUV 色彩空间转换 API,覆…

2026/9/23 23:12:37 阅读更多 →
Rust的Option与Result:零成本抽象下的类型安全与错误处理

Rust的Option与Result:零成本抽象下的类型安全与错误处理

1. 先从一次“空指针崩溃”说起:Option 到底在替我们挡什么如果你写过一阵 C 或者 C,大概率经历过这样的场景:某个函数返回一个指针,你心里清楚它可能为空,但接口文档没写,调用方也懒得判断,于是…

2026/9/23 23:11:37 阅读更多 →

日新闻

3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A…

2026/9/23 0:00:23 阅读更多 →
2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我

2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我

2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我 刚把开发环境的显示器从1080P换到2K,跑老项目直接报错,版本升级后 API…

2026/9/23 0:01:25 阅读更多 →
3步搞定美眉图实战项目,告别官方文档抓不住重点

3步搞定美眉图实战项目,告别官方文档抓不住重点

3步搞定美眉图实战项目,告别官方文档抓不住重点 官方文档翻了三遍还是云里雾里?别急,美眉图在实战项目中常被用来做数据可视化,但它的原理比你想的简单。今天咱们直接上手,用一个完整的小项目把美眉图跑通,不再死磕那些冗长的理论说明。…

2026/9/23 0:01:25 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/23 4:49:06 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →