@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 markRaw<T> { (target: T): T }- 入参:任意对象或类(构造函数/函数)。
- 返回值:原对象本身(原地打标记后返回),不产生新对象、不破坏引用。
从源码看,它由 externals.ts 导出,并经由 index.ts 的export * from './externals'对外提供,最终从包入口直接import { markRaw } from '@formily/reactive'使用。
三、核心原理:RAW_TYPE 标记与劫持判定
3.1 两个私有 Symbol
在 externals.ts 中,仓库定义了两个内部 Symbol:
const 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())会正常创建 Proxy,a.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]返回false,createObservable直接原样返回该对象,不创建 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_TYPE(externals.ts)。
两者同时存在时规则如何?单元测试给出了明确答案(externals.spec.ts):
test('plain object marked as raw and observable should NOT be observable', () => { const obs = observable<any>(markRaw(markObservable({ aa: 111 }))) expect(isObservable(obs)).toBe(false) }) test('plain object marked as observable and raw should NOT be observable', () => { const obs = observable<any>(markObservable(markRaw({ aa: 111 }))) expect(isObservable(obs)).toBe(false) })无论标记顺序如何(先 markObservable 再 markRaw,或反之),最终结果都是不可劫持——因为isSupportObservable永远先读RAW_TYPE。这正是"优先级比 markObservable 高"的完整语义:
| 标记组合 | isSupportObservable 结果 | 是否被劫持 |
|---|---|---|
| 无任何标记 | 视对象类型而定 | 默认逻辑 |
| 仅 markRaw | false | 否 |
| 仅 markObservable | true | 是 |
| markRaw + markObservable(任意顺序) | false | 否 |
六、与 toJS 的交互:已劫持对象再打标记的注意点
文档中有一条容易被忽略的注意事项:
注意:如果对一个已经是 observable 的对象标记 markRaw,那么 toJS,是不会将它转换成普通对象的
理解这一点需要看toJS的实现(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 // 已标记 raw → 原样返回 if (isArr(values)) { if (isObservable(values)) { // 递归解包为普通数组 } } else if (isPlainObj(values)) { if (isObservable(values)) { // 递归解包为普通对象 } } return values } return _toJS(values) }toJS的核心目标是"把 observable 递归转换为普通数据结构"。但当一个对象已经是 observable(Proxy)之后再被markRaw标记,它同时满足两个条件:
values[RAW_TYPE]为 true(在 proxy 原对象上打了标记,通过 proxy 可读到);- 它本身仍然是 observable(Proxy 早已创建)。
由于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 直接返回true,markRaw 对它们无效; - 优先级铭记于心:raw 永远压过 observable,两者同标时 raw 胜出。
7.3 验证入口
- API 文档:markRaw.zh-CN.md
- 源码实现:externals.ts(
markRaw/isSupportObservable/toJS) - 劫持入口:internals.ts(
createObservable) - 单元测试:externals.spec.ts(mark 优先级、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),仅供参考