news 2026/9/24 21:41:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@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 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')

markRawmarkObservable的全部行为,本质就是对这两个 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 = 123

markRaw(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_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 结果是否被劫持
无任何标记视对象类型而定默认逻辑
仅 markRawfalse
仅 markObservabletrue
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标记,它同时满足两个条件:

  1. values[RAW_TYPE]为 true(在 proxy 原对象上打了标记,通过 proxy 可读到);
  2. 它本身仍然是 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 实践建议

  1. 标记越早越好:在对象进入响应式图之前(observable调用前)完成标记,才能获得完整豁免语义;
  2. 类级标记要克制markRaw(A)影响该类的所有后续实例,属于全局行为,仅在确实"任何实例都不该被劫持"时使用;
  3. 勿对已劫持对象补标:它无法撤销已生成的 Proxy,且会让toJS返回 observable 代理而非普通对象;
  4. 数组与集合不可用isSupportObservableisArr与 Map/Set 直接返回true,markRaw 对它们无效;
  5. 优先级铭记于心: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 21:40:40

U2Net轻量化实战:分组卷积压缩至86M,边缘端SOD部署指南

简介&#xff1a;本资源是一套面向计算机视觉初学者与进阶研究者的非特定类别图像分割实践项目&#xff0c;聚焦显著性目标检测&#xff08;SOD&#xff09;在通用图像分割中的落地应用&#xff0c;特别适配轻量化部署需求。项目基于U2Net模型展开深度优化实验&#xff0c;完整…

作者头像 李华
网站建设 2026/9/24 21:40:40

中文NER实战:BERT+BiLSTM+CRF源码解析与课程设计指南

简介&#xff1a;基于BERTBiLSTMCRF实现中文命名实体识别的Python源码&#xff0c;面向需要完成课程设计或期末大作业的高校学生&#xff0c;也适合正在学习自然语言处理与序列标注的开发者。项目覆盖数据预处理、模型训练到指标评估的完整流程&#xff0c;下载解压即可运行&am…

作者头像 李华
网站建设 2026/9/24 21:40:33

His标签蛋白纯化全流程解析:从菌体破碎到凝胶层析

蛋白纯化是生物实验室的高频操作&#xff0c;但不同来源、不同性质的蛋白&#xff0c;纯化策略差异很大&#xff0c;翻车概率也不小。这篇就以一个His标签蛋白纯化项目为例&#xff0c;把从菌体破碎到凝胶层析的完整路线、每个环节的设计逻辑、常见坑位都拆开讲清楚&#xff0c…

作者头像 李华
网站建设 2026/9/24 21:40:30

从回归到排序:构建与人类偏好对齐的AI人脸吸引力模型

最近在Hacker News上逛的时候&#xff0c;看到一个「Show HN」项目&#xff1a;AI Facial Attractiveness Model Aligned with Human Preferences&#xff0c;通俗说就是做一个“用人类偏好对齐出来的AI人脸吸引力模型”。这个方向我过去大半年一直在折腾&#xff0c;从第一批d…

作者头像 李华
网站建设 2026/9/24 21:39:45

Excel加载宏与XLL插件:从原理到工业级开发实战

1. 这不是“点几下就完事”的功能——Excel加载宏与XLL插件的本质是什么&#xff1f;你可能在Excel菜单栏里见过“开发工具”选项卡&#xff0c;点开后看到“加载项”按钮&#xff0c;旁边还写着“管理Excel加载项…”&#xff1b;也可能在某个技术论坛里被一句“用XLL写个高性…

作者头像 李华
网站建设 2026/9/24 21:39:45

从零构建本地AI平台:模型管理、知识库与API网关的架构实践

如果你只是想在本机跑一个大模型聊聊天&#xff0c;现在的工具其实多到有点挑花眼&#xff1b;但如果你想要的是一个开源的、完全运行在本地、又能当正经生产力工具的 AI 平台——能管理模型、能挂知识库、能调用工具、能同时给几个人用、还能对外提供标准 API——你会发现市面…

作者头像 李华