ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

rrdom:为 rrweb 回放引擎打造的虚拟 DOM 库

2026/9/21 1:29:17 拓冰建站 浏览量
rrdom:为 rrweb 回放引擎打造的虚拟 DOM 库 rrdom为 rrweb 回放引擎打造的虚拟 DOM 库【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrwebrrdom 是 rrweb 项目中负责「回放 DOM 变更」的核心虚拟 DOM 库它既能独立运行用于构造一棵虚拟 DOM 树并将补丁应用到真实 DOM也是 rrweb 回放器在快进、拖拽seek场景下优化渲染性能的关键基础设施。读完本文你将理解 rrdom 的节点体系、Mirror 映射机制、diff 算法以及它在 rrweb 回放器 中的实际接入方式并掌握独立安装与使用它的方法。rrdom 在 rrweb 生态中的定位rrdom 的 README 对自己的定位表述得非常明确rrdom 是一个虚拟 DOM 库被 rrweb 用于回放 DOM mutations。它是一个独立库可以用来创建虚拟 DOM 树并把补丁应用到真实 DOM。rrweb 使用它来优化回放性能尤其是在 seek时间轴跳转/快进时。这句话拆开看包含三个关键事实它是虚拟 DOM 库与 React 的虚拟 DOM 思想类似先在内存中维护一棵轻量级的 DOM 树RRDocument/RRElement等节点再通过 diff 算法把增量变化映射到真实 DOM服务于回放replay而非录制record录制的产物是一系列序列化事件回放时要把这些事件重新变成真实页面状态rrdom 负责其中「节点级结构变更」的部分独立可复用虽然它是 rrweb monorepo 的一个子包packages/rrdom但其 API 设计不依赖回放器本身可以脱离 rrweb 单独使用。整个 rrweb 项目的工作流可以参考根目录的 guide.mdrrdom 属于回放链路replay一侧的组件。整体架构一棵「可补丁」的虚拟 DOM 树rrdom 的源码只有四个核心文件结构非常精简文件职责packages/rrdom/src/document.ts虚拟节点基类与接口定义IRRNode、IRRDocument、IRRElement等packages/rrdom/src/index.tsRRDocument、各元素子类、Mirror、buildFromDom/buildFromNode等对外入口packages/rrdom/src/diff.tsdiff 算法与createOrGetNode负责把虚拟树应用到真实 DOMpackages/rrdom/src/style.tsCSS 文本与 camelCase 样式对象之间的互转工具index.ts对外导出的核心符号包括RRDocument、RRElement、RRMediaElement、RRCanvasElement、RRStyleElement、RRIFrameElement、RRDialogElement、RRText、RRComment、RRCDATASection、Mirror、createMirror、buildFromDom、buildFromNode、getDefaultSN、printRRDom、diff、createOrGetNode以及ReplayerHandler类型。节点类型体系虚拟 DOM 树由BaseRRNode的各个子类构成节点类型与标准 DOM 一一对应见 document.ts 中的NodeType枚举取值与标准Node.nodeType一致BaseRRDocument文档节点维护documentElement/head/body的访问器通过遍历子节点查找HTML、HEAD、BODY标签并保证同一文档内「只允许一个 RRElement 或一个 RRDoctype」BaseRRDocumentTypedoctype 节点保存name/publicId/systemIdBaseRRElement元素节点用Recordstring, string保存属性提供classList、id、className、style等与真实元素对齐的接口其中style的 getter 会调用 style.ts 的parseCSSText把style...属性文本解析成 camelCase 对象setProperty/removeProperty则反向用toCSSText写回BaseRRText/BaseRRComment/BaseRRCDATASection文本、注释与 CDATA 节点BaseRRMediaElement媒体元素额外携带currentTime、volume、paused、muted、playbackRate、loop等回放状态字段BaseRRDialogElementdialog元素用私有属性rr_open_mode区分show()非模态与showModal()模态两种打开方式。所有节点都实现了appendChild/insertBefore/removeChild/contains等树操作这些操作在 document.ts 底部的工具函数中完成双向链表式维护。需要被 diff 算法「特殊照顾」的元素子类在RRDocument.createElement的工厂方法中index.ts部分标签会被实例化为专门子类标签子类附加数据AUDIO/VIDEORRMediaElement播放状态字段见上IFRAMERRIFrameElement内嵌一棵contentDocument虚拟文档CANVASRRCanvasElementrr_dataURL初始图像、canvasMutations变更队列STYLERRStyleElementrules样式表规则数组DIALOGRRDialogElement模态状态这些子类存在的意义在于真实浏览器里这些元素具有「特殊行为」无法仅靠属性/子节点表达完整状态例如 canvas 的绘图内容、video 的播放位置、dialog 的模态层因此 diff 算法在应用补丁时必须对它们做定向处理见下文。从真实 DOM 构建虚拟树buildFromDom 与 buildFromNode要计算「旧树 → 新树」的差异首先得把某一时刻的真实 DOM 快照转换成虚拟树。这正是 index.ts 中buildFromNode与buildFromDom的职责buildFromNode(node, rrdom, domMirror, parentRRNode?)把单个真实Node转换为对应的RRNode。它按nodeType分派文档节点若父节点是IFRAME则复用RRIFrameElement.contentDocument否则复用rrdom本身并同步compatModeBackCompat/CSS1Compat元素节点读取tagName对HTMLFormElement有特殊处理见 index.ts逐项拷贝attributes并记录scrollLeft/scrollTop文本/注释/CDATA直接创建对应虚拟节点DOCUMENT_FRAGMENT_NODE即 shadow root调用attachShadow({ mode: open })挂到父虚拟元素上其余类型返回null。buildFromDom(dom, domMirror?, rrdom?)从根开始递归遍历整棵真实文档树含 iframe 的contentDocument、元素的shadowRoot构建完整虚拟树默认返回新的RRDocument。值得一提的是 index.ts 的「未序列化节点」设计回放器中存在一些未经过序列化的事件例如注入样式规则用的 style 元素它们的 id 从-2开始递减unserializedId与序列化节点id 0区分开从而避免干扰 diff 算法的节点匹配。测试 test/virtual-dom.test.ts 中「can patch serialized ID for an unserialized node」和「can access a unique, decremented unserializedId every time」两个用例直接验证了这一机制。Mirror连接真实 DOM 与虚拟 DOM 的桥梁diff 算法要高效工作必须快速回答「这个真实节点对应哪个虚拟节点」。rrdom 为此维护了两套镜像NodeMirror来自 rrweb-snapshot记录真实 DOM 节点 → 序列化数据serializedNodeWithId的映射属于回放器的既有设施Mirrorrrdom 自带记录虚拟RRNode→ 序列化数据的映射实现在 index.ts内部用Mapnumber, RRNode存 id→节点、用WeakMapRRNode, serializedNodeWithId存节点→元数据。Mirror提供getId/getNode/getMeta/has/hasNode/add/replace/removeNodeFromMap/reset/getIds等 API。核心约定是同一 id 在两边镜像中指向「逻辑上相同」的节点diff 算法正是依靠 id 相等性来判断节点是否匹配见nodeMatchingdiff.ts。RRDocument默认自带一个mirror实例也可以通过构造函数传入外部 mirror 共享index.tsiframe 的contentDocument会与父文档共享同一个 mirror。diff 算法把虚拟树补丁应用到真实 DOMdiff 的核心实现在 packages/rrdom/src/diff.ts入口签名如下export function diff( oldTree: Node, // 真实 DOM 树被修改的目标 newTree: IRRNode, // 虚拟 DOM 树期望状态 replayer: ReplayerHandler,// 回放器回调集合 rrnodeMirror?: Mirror, // 虚拟树的 mirror )整个过程分为三个阶段对应三个函数1. diffBeforeUpdatingChildren更新「自身」属性先处理节点自身属性、样式、滚动位置等再处理子节点。这样做的原因在源码注释中有明确说明如果父节点的样式/属性影响子节点高度而applyScroll又依赖正确高度那么先更新父节点属性才能保证滚动位置计算正确diff.ts。该阶段还会处理两个前置问题节点类型不一致时的校准如果新旧树对应位置的节点类型不同如sameNodeType返回 false先用createOrGetNode创建正确的真实节点替换旧节点避免后续 diff 出错Document 节点的特殊情况当 iframe 的 contentDocument 被浏览器自动挂载了 html/head/body或者新文档的序列化 id 与旧文档不一致时需要关闭再重新 open 文档并同步更新 NodeMirrordiff.ts。2. diffChildren子节点列表的增删移子节点对比采用了双端指针 id 哈希表的经典 diff 策略类似 Vue 的 diff 思路diff.ts依次比较「旧首 vs 新首」「旧尾 vs 新尾」「旧首 vs 新尾」「旧尾 vs 新首」四对组合能匹配就移动指针避免不必要的重建若四对都不匹配则为旧子节点建立id → 下标的哈希表尝试按 id 找到可移动的节点找不到才调用createOrGetNode创建新节点循环结束后如果旧树还有剩余就删除并同步mirror.removeNodeFromMap如果新树还有剩余就批量插入最后递归地对逐对子节点再次调用diff完成深层结构的同步。其中还处理了两个文档标准限制的边界情况同一文档不允许同时存在两个 doctype也不允许两个 HTML 根元素因此插入新节点前要先移除旧的diff.ts。3. diffAfterUpdatingChildren应用「事后」效果某些节点状态必须在子节点更新完成后才能正确应用diff.tsDocument / Element应用scrollData滚动位置元素再应用inputData输入值——注释说明如果 select 的 options 还没填充就设置 value 会失效AUDIO / VIDEO同步paused、muted、volume、currentTime、playbackRate、loopCANVAS若存在rr_dataURLiframe 场景下的初始图像数据见 diff.ts先绘制图像再按序重放canvasMutations中的绘制指令STYLE在子节点更新后再应用rules避免 textContent 覆盖属性diff.tsDIALOG对比新旧open与模态状态决定调用close()/show()/showModal()并捕获异常避免中断渲染。此外ReplayerHandler.afterAppend回调会在新插入节点上以后序遍历顺序触发createdNodeSet弱集合保证顺序与 rrweb-snapshot 包一致diff.tsrrweb 回放器用它来通知各插件执行onBuild钩子。createOrGetNode虚拟节点 → 真实节点当 diff 需要插入一个新节点时调用 diff.ts 的createOrGetNode先查 NodeMirror若该 id 对应的真实节点已存在且类型一致直接复用否则按虚拟节点类型创建真实节点SVG 元素走createElementNS利用SVGTagMap把clippath之类的 tagName 还原为 camelCase并正确设置xlink:href等命名空间见 diff.ts普通元素走createElement创建后把序列化数据写入 NodeMirror并登记到createdNodeSet以便触发afterAppend。回放器如何消费 rrdomuseVirtualDom 与 seek 优化rrdom 真正发挥威力的场景是快进fast-forward / seek。在 rrweb 回放器 中回放器持有public virtualDom: RRDocument new RRDocument()replay/index.ts默认配置项useVirtualDom: true即虚拟 DOM 优化默认开启replay/index.ts可在playerConfig中关闭类型定义见 packages/rrweb/src/types.ts在applyMutation中replay/index.ts只有当useVirtualDom 开启、尚未启用虚拟 DOM、且正在同步快进isSync三者同时满足时才启动优化把真实 iframe 文档整树构建为虚拟树buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom)快进过程中产生的节点 mutation 只修改虚拟树成本远低于直接操作真实 DOM快进结束后一次性调用diff(this.iframe.contentDocument, this.virtualDom, replayerHandler, this.virtualDom.mirror)把累积的差异批量应用到真实 DOMreplay/index.ts随后destroyTree()复位index.ts 会清空子节点并mirror.reset()。源码注释解释了为什么只在「节点类 mutation 同步快进」时启用创建虚拟树并执行 diff 的成本通常高于直接应用其他类型的事件如鼠标、滚动因此优化只针对最耗时的场景replay/index.ts。回放器把自身能力以ReplayerHandler的形式注入 diff 过程replay/index.ts 中构造的replayerHandler包含applyCanvas、applyInput、applyScroll、applyStyleSheetMutation和afterAppend这样 rrdom 只负责结构 diff而把 canvas 绘制、输入回填、滚动恢复等「副作用」委托给回放器执行——这也是 rrdom 能与 rrweb 解耦的关键设计。独立安装与使用rrdom 是发布到 npm 的独立包当前版本 2.1.5见 packages/rrdom/package.json可通过包管理器直接安装yarn add rrdom # 或 npm install rrdom包同时提供 ESMdist/rrdom.js、CJSdist/rrdom.cjs与 UMDdist/rrdom.umd.cjs同时配置了 unpkg/jsdelivr CDN 入口并导出 TypeScript 类型声明。它唯一的运行时依赖是rrweb-snapshot用于复用NodeMirror与序列化类型。一个最小的独立使用示例——从当前页面构建虚拟树并打印import { RRDocument, buildFromDom } from rrdom; const virtualDom new RRDocument(); buildFromDom(document, undefined, virtualDom); console.log(virtualDom.documentElement?.tagName); // HTML console.log(virtualDom.head?.tagName); // HEAD console.log(virtualDom.body?.tagName); // BODY调试时可使用printRRDom(rootNode, mirror)index.ts以带缩进的文本形式输出整棵虚拟树含 shadowRoot 与 iframe 子文档测试用例也大量依赖它做快照断言。测试与正确性保障rrdom 的测试packages/rrdom/test覆盖了三个层面test/virtual-dom.test.ts在 jsdom 与真实浏览器puppeteer中验证节点构建——包括 quirks 模式文档的compatMode同步、未序列化节点的负 id 分配、滚动位置采集、iframecontentDocument与 shadow DOM 的构建、RRDocument的增删开合与 mirror 全量 API以及对 main.html / iframe.html / shadow-dom.html / XML 页面的构建快照test/diff.test.ts用createTree辅助函数按描述性数据结构生成新旧两棵树覆盖单节点 diff、子节点增删移、doctype/根元素冲突等边界并严格要求测试过程中不触发console.warn一旦 diff 抛错会冒泡到ReplayerEvents.Flush监听器中断渲染test/snapshots/virtual-dom.test.ts.snap虚拟树打印结果的快照基线。通过yarn testvitest即可运行整个包的全部用例。小结rrdom 以约四个源文件的精简体积为 rrweb 回放器提供了「虚拟树构建 → id 镜像映射 → diff 补丁 → 特殊元素副作用」的完整闭环构建buildFromDom/buildFromNode把真实 DOM含 iframe、shadow DOM镜像为虚拟树匹配Mirror双镜像机制用序列化 id 建立真实节点与虚拟节点的对应关系补丁diff采用双端指针 id 哈希策略高效同步子节点并针对 media/canvas/style/dialog 等特殊元素在事后阶段应用状态接入回放器通过useVirtualDom默认开启把虚拟 DOM 优化限定在「同步快进 节点 mutation」这一最高开销场景seek 结束后一次性落盘到真实 DOM。如果你正在研究会话回放session replay的性能优化或者需要一个可在浏览器环境独立运行、可对真实 DOM 应用补丁的轻量虚拟 DOM 实现rrdom 的源码document.ts、diff.ts、index.ts与测试都是值得精读的参考。【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考