- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
导读
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 toJS<T> { (target: T): T }即:输入什么类型的值,就返回什么类型的值,泛型T保证类型在转换前后保持不变。注意它只接受一个参数,深度转换是默认行为,无需额外配置。
官方文档给出的基础用例(见 toJS.zh-CN.md):
import { 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 WeakSet<any>() 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 = observable<any>(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] }即:对象要么是ProxyRaw(Proxy → raw 映射,见 environment.ts)中登记过的 Proxy,要么是带ObModelSymbol的模型对象,二者都算 observable。
3. markRaw 的短路:RAW_TYPE 标记
toJS内部有一行非常关键的判断:
if (values && values[RAW_TYPE]) return valuesRAW_TYPE是一个模块级私有 Symbol(externals.ts),由markRaw写入。这就是文档警告"对已 observable 对象标记 markRaw 后,toJS 不会转换它"的实现根源:一旦对象带有RAW_TYPE标记,toJS会直接把它原样返回,不做任何深拷贝。
四、与 markRaw 的联动:理解 RAW_TYPE 的语义
markRaw的官方语义是"标记任意一个对象或者类原型为永远不可被 observable 劫持,优先级比 markObservable 高"(见 markRaw.zh-CN.md)。其实现同样在 externals.ts:
export 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为 false);markRaw(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.ts):
- React 元素(带
$$typeof与_owner)、moment 对象、JSON Schema 对象会被自动排除,避免被代理破坏内部状态; - 自带
toJS/toJSON方法的对象不会被代理,从而保留自定义序列化能力(如 moment 的toJSON); - Map / WeakMap / Set / WeakSet 始终支持 observable,
null、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 对象 |
| 签名 | toJS<T>(target: T): T |
| 递归 | 支持数组、普通对象深度遍历,WeakSet防循环引用 |
| markRaw 联动 | 带RAW_TYPE标记的对象原样返回,不转换 |
| 递归出口 | 非 observable 值原样返回 |
| 源码位置 | packages/reactive/src/externals.ts |
| 测试验证 | packages/reactive/src/tests/externals.spec.ts |
toJS虽小,却是响应式数据"走出" @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
相关推荐
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),仅供参考