ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

@formily/reactive 的 markRaw:彻底掌控响应式劫持边界的权威指南

@formily/reactive 的 markRaw:彻底掌控响应式劫持边界的权威指南 formily/reactive 的 markRaw彻底掌控响应式劫持边界的权威指南【免费下载链接】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导读markRaw是formily/reactive响应式体系中的隔离阀它能够标记任意对象或类原型为永远不可被 observable 劫持从而把第三方库实例、DOM/React 节点、全局配置等对象排除在响应式追踪之外。本文以 markRaw 官方文档 为核心骨架结合packages/reactive包的源码实现与单元测试系统讲解 markRaw 的签名、实例级/类级两种标记方式、与 markObservable 的优先级关系、与 toJS 的交互行为并给出可运行用例与源码级原理印证。一、markRaw 是什么响应式世界的豁免权在formily/reactive中observable会把普通对象包装成 Proxy从而拦截属性的读写并触发依赖收集与更新。但并非所有对象都适合被劫持外部库实例如富文本编辑器、地图实例、Canvas 上下文内部状态复杂劫持会产生大量无意义开销甚至破坏其内部实现只读配置对象根本不会变化无需进入响应式依赖图React/Vue 节点、moment 对象、带 toJSON/toJS 方法的对象仓库默认就跳过劫持见下文isSupportObservable。markRaw正是为用户提供手动豁免能力的 API标记之后该对象或该类的所有实例在任何时刻、任何位置被observable处理时都会被直接原样返回不再创建 Proxy。官方文档对它的定位非常精炼标记任意一个对象或者类原型为永远不可被 observable 劫持优先级比 markObservable 高这段描述包含了两个关键语义永远一旦标记后续任何劫持尝试都失效与**优先级更高**同时存在两种标记时raw 生效。二、函数签名与导出markRaw的签名如下与文档一致interface markRawT { (target: T): T }入参任意对象或类构造函数/函数。返回值原对象本身原地打标记后返回不产生新对象、不破坏引用。从源码看它由 externals.ts 导出并经由 index.ts 的export * from ./externals对外提供最终从包入口直接import { markRaw } from formily/reactive使用。三、核心原理RAW_TYPE 标记与劫持判定3.1 两个私有 Symbol在 externals.ts 中仓库定义了两个内部 Symbolconst RAW_TYPE Symbol(RAW_TYPE) const OBSERVABLE_TYPE Symbol(OBSERVABLE_TYPE)markRaw与markObservable的全部行为本质就是对这两个 Symbol 的写入与读取。3.2 markRaw 的实现细节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 }关键设计传函数类时标记在prototype上传普通对象时标记在对象自身。这正是类级标记让所有实例生效的底层来源——实例在原型链上就能读到该标记。3.3 劫持前的资格检查每次observable创建代理前都会经过 internals.ts 中的createObservable调用isSupportObservable做资格检查export const isSupportObservable (target: any) { if (!isValid(target)) return false if (isArr(target)) return true if (isPlainObj(target)) { if (target[RAW_TYPE]) { return false // ① 已标记 raw → 直接拒绝劫持 } if (target[OBSERVABLE_TYPE]) { return true // ② 已标记 observable → 强制劫持 } if ($$typeof in target _owner in target) return false // React Node 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 } // Map / WeakMap / Set / WeakSet 恒可劫持 if (isMap(target) || isWeakMap(target) || isSet(target) || isWeakSet(target)) return true return false }注意RAW_TYPE检查位于OBSERVABLE_TYPE检查之前见 externals.ts这就是markRaw 优先级比 markObservable 高的源码级证据即使同时打了两个标记isSupportObservable也会先命中 RAW_TYPE 返回false。另外数组isArr与 Map/Set 等集合类型不经过RAW_TYPE 检查因此对数组调用markRaw不会生效——这一点在测试isSupportObservable([])返回true中也可得到印证见 externals.spec.ts。四、三种典型用法从实例到类文档中的用例覆盖了三个递进场景此处完整继承并补充运行逻辑说明。4.1 基线普通对象被正常劫持import { observable, autorun, markRaw } from formily/reactive class A { property } const a observable(new A()) autorun(() { console.log(a.property) // property 变化时会被触发因为 A 实例是普通对象 }) a.property 123由于A既没有toJSON/toJS方法也没有任何标记observable(new A())会正常创建 Proxya.property 123会触发上方autorun重新执行。4.2 实例级标记只豁免当前实例const b observable(markRaw(new A())) // 实例级标记只对当前实例生效 autorun(() { console.log(b.property) // property 变化时不会被触发因为已被标记 raw }) b.property 123markRaw(new A())先在实例自身上写入RAW_TYPE true随后observable调用isSupportObservable时命中target[RAW_TYPE]返回falsecreateObservable直接原样返回该对象不创建 Proxy因此对b.property的赋值不会触发任何响应式订阅。该类的其他新实例不受影响仍可正常劫持。4.3 类级标记所有实例全部豁免markRaw(A) // 类级标记那么所有实例都会生效 const c observable(new A()) autorun(() { console.log(c.property) // property 变化时不会被触发因为已被标记 raw }) c.property 123由于markRaw对函数类会写入A.prototype[RAW_TYPE]此后new A()的每个实例都能在原型链上读到该标记从而被永久豁免。这是一个全局性、影响所有后续实例的操作使用时需评估是否会影响其他依赖该类响应式行为的代码。五、与 markObservable 的优先级对抗markObservable是 markRaw 的对偶 API见 markObservable 文档它用于强制劫持那些默认会被跳过的对象例如带toJSON方法的类。其实现同样写入OBSERVABLE_TYPEexternals.ts。两者同时存在时规则如何单元测试给出了明确答案externals.spec.tstest(plain object marked as raw and observable should NOT be observable, () { const obs observableany(markRaw(markObservable({ aa: 111 }))) expect(isObservable(obs)).toBe(false) }) test(plain object marked as observable and raw should NOT be observable, () { const obs observableany(markObservable(markRaw({ aa: 111 }))) expect(isObservable(obs)).toBe(false) })无论标记顺序如何先 markObservable 再 markRaw或反之最终结果都是不可劫持——因为isSupportObservable永远先读RAW_TYPE。这正是优先级比 markObservable 高的完整语义标记组合isSupportObservable 结果是否被劫持无任何标记视对象类型而定默认逻辑仅 markRawfalse否仅 markObservabletrue是markRaw markObservable任意顺序false否六、与 toJS 的交互已劫持对象再打标记的注意点文档中有一条容易被忽略的注意事项注意如果对一个已经是 observable 的对象标记 markRaw那么 toJS是不会将它转换成普通对象的理解这一点需要看toJS的实现externals.tsexport 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 // 已标记 raw → 原样返回 if (isArr(values)) { if (isObservable(values)) { // 递归解包为普通数组 } } else if (isPlainObj(values)) { if (isObservable(values)) { // 递归解包为普通对象 } } return values } return _toJS(values) }toJS的核心目标是把 observable 递归转换为普通数据结构。但当一个对象已经是 observableProxy之后再被markRaw标记它同时满足两个条件values[RAW_TYPE]为 true在 proxy 原对象上打了标记通过 proxy 可读到它本身仍然是 observableProxy 早已创建。由于toJS第一层判断优先命中RAW_TYPE直接return values返回的仍是那个 observable 代理本身而不是解包后的普通对象。所以正确的隔离顺序是先 markRaw、再 observable本文第四节的所有用例如果反过来先 observable 再 markRaw虽然能阻止未来的进一步劫持但已生成的 Proxy 不会被撤销toJS也不会帮它还原成普通对象。若需要还原应在标记前调用toJS完成转换。七、真实场景定位与最佳实践7.1 典型适用场景结合isSupportObservable默认豁免清单React Node、moment、toJSON/toJS 携带者与 markRaw 的能力以下场景最适合使用 markRaw集成第三方 UI/渲染库实例如编辑器、图表实例防止内部状态被劫持只读的全局配置/常量对象避免无意义的依赖收集与框架 Node 混用的自定义对象当对象形似 React Node含$$typeof/_owner但语义不同时用 markRaw 显式声明对象图内部不需要响应式的缓存节点显式标记可提升性能与可预测性。7.2 实践建议标记越早越好在对象进入响应式图之前observable调用前完成标记才能获得完整豁免语义类级标记要克制markRaw(A)影响该类的所有后续实例属于全局行为仅在确实任何实例都不该被劫持时使用勿对已劫持对象补标它无法撤销已生成的 Proxy且会让toJS返回 observable 代理而非普通对象数组与集合不可用isSupportObservable对isArr与 Map/Set 直接返回truemarkRaw 对它们无效优先级铭记于心raw 永远压过 observable两者同标时 raw 胜出。7.3 验证入口API 文档markRaw.zh-CN.md源码实现externals.tsmarkRaw/isSupportObservable/toJS劫持入口internals.tscreateObservable单元测试externals.spec.tsmark 优先级、toJS 递归等全部用例结语markRaw虽是一个小 API却是formily/reactive响应式边界治理的关键拼图它以两个 Symbol 标记实现了永远豁免的强语义通过与markObservable的优先级对抗提供了双向控制能力并与toJS的交互形成了先标记、后劫持的黄金实践。理解其源码实现externals.ts与测试约束你就能在集成第三方库、优化性能与维护可预测性之间游刃有余。【免费下载链接】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),仅供参考
返回列表