news 2026/9/21 1:29:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
rrdom:为 rrweb 回放引擎打造的虚拟 DOM 库

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

【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb

rrdom 是 rrweb 项目中负责「回放 DOM 变更」的核心虚拟 DOM 库:它既能独立运行,用于构造一棵虚拟 DOM 树并将补丁应用到真实 DOM,也是 rrweb 回放器在快进、拖拽(seek)场景下优化渲染性能的关键基础设施。读完本文,你将理解 rrdom 的节点体系、Mirror 映射机制、diff 算法以及它在 rrweb 回放器 中的实际接入方式,并掌握独立安装与使用它的方法。

rrdom 在 rrweb 生态中的定位

rrdom 的 README 对自己的定位表述得非常明确:

rrdom 是一个虚拟 DOM 库,被 rrweb 用于回放 DOM mutations。它是一个独立库,可以用来创建虚拟 DOM 树,并把补丁应用到真实 DOM。rrweb 使用它来优化回放性能,尤其是在 seek(时间轴跳转/快进)时。

这句话拆开看包含三个关键事实:

  1. 它是虚拟 DOM 库:与 React 的虚拟 DOM 思想类似,先在内存中维护一棵轻量级的 DOM 树(RRDocument/RRElement等节点),再通过 diff 算法把增量变化映射到真实 DOM;
  2. 服务于回放(replay)而非录制(record):录制的产物是一系列序列化事件,回放时要把这些事件重新变成真实页面状态,rrdom 负责其中「节点级结构变更」的部分;
  3. 独立可复用:虽然它是 rrweb monorepo 的一个子包(packages/rrdom),但其 API 设计不依赖回放器本身,可以脱离 rrweb 单独使用。

整个 rrweb 项目的工作流可以参考根目录的 guide.md,rrdom 属于回放链路(replay)一侧的组件。

整体架构:一棵「可补丁」的虚拟 DOM 树

rrdom 的源码只有四个核心文件,结构非常精简:

文件职责
packages/rrdom/src/document.ts虚拟节点基类与接口定义(IRRNodeIRRDocumentIRRElement等)
packages/rrdom/src/index.tsRRDocument、各元素子类、Mirror、buildFromDom/buildFromNode等对外入口
packages/rrdom/src/diff.tsdiff 算法与createOrGetNode,负责把虚拟树应用到真实 DOM
packages/rrdom/src/style.tsCSS 文本与 camelCase 样式对象之间的互转工具

index.ts对外导出的核心符号包括:RRDocumentRRElementRRMediaElementRRCanvasElementRRStyleElementRRIFrameElementRRDialogElementRRTextRRCommentRRCDATASectionMirrorcreateMirrorbuildFromDombuildFromNodegetDefaultSNprintRRDomdiffcreateOrGetNode,以及ReplayerHandler类型。

节点类型体系

虚拟 DOM 树由BaseRRNode的各个子类构成,节点类型与标准 DOM 一一对应(见 document.ts 中的NodeType枚举,取值与标准Node.nodeType一致):

  • BaseRRDocument:文档节点,维护documentElement/head/body的访问器(通过遍历子节点查找HTMLHEADBODY标签),并保证同一文档内「只允许一个 RRElement 或一个 RRDoctype」;
  • BaseRRDocumentType:doctype 节点,保存name/publicId/systemId
  • BaseRRElement:元素节点,用Record<string, string>保存属性,提供classListidclassNamestyle等与真实元素对齐的接口;其中style的 getter 会调用 style.ts 的parseCSSTextstyle="..."属性文本解析成 camelCase 对象,setProperty/removeProperty则反向用toCSSText写回;
  • BaseRRText/BaseRRComment/BaseRRCDATASection:文本、注释与 CDATA 节点;
  • BaseRRMediaElement:媒体元素,额外携带currentTimevolumepausedmutedplaybackRateloop等回放状态字段;
  • BaseRRDialogElement<dialog>元素,用私有属性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 中buildFromNodebuildFromDom的职责:

  • 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 为此维护了两套镜像

  1. NodeMirror(来自 rrweb-snapshot):记录真实 DOM 节点 → 序列化数据(serializedNodeWithId)的映射,属于回放器的既有设施;
  2. Mirror(rrdom 自带):记录虚拟RRNode→ 序列化数据的映射,实现在 index.ts,内部用Map<number, RRNode>存 id→节点、用WeakMap<RRNode, serializedNodeWithId>存节点→元数据。

Mirror提供getId/getNode/getMeta/has/hasNode/add/replace/removeNodeFromMap/reset/getIds等 API。核心约定是:同一 id 在两边镜像中指向「逻辑上相同」的节点,diff 算法正是依靠 id 相等性来判断节点是否匹配(见nodeMatching,diff.ts)。

RRDocument默认自带一个mirror实例,也可以通过构造函数传入外部 mirror 共享(index.ts),iframe 的contentDocument会与父文档共享同一个 mirror。

diff 算法:把虚拟树补丁应用到真实 DOM

diff 的核心实现在 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 文档,并同步更新 NodeMirror(diff.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.ts):

  • Document / Element:应用scrollData(滚动位置);元素再应用inputData(输入值——注释说明:如果 select 的 options 还没填充就设置 value 会失效);
  • AUDIO / VIDEO:同步pausedmutedvolumecurrentTimeplaybackRateloop
  • CANVAS:若存在rr_dataURL(iframe 场景下的初始图像数据,见 diff.ts),先绘制图像,再按序重放canvasMutations中的绘制指令;
  • STYLE:在子节点更新后再应用rules,避免 textContent 覆盖属性(diff.ts);
  • DIALOG:对比新旧open与模态状态,决定调用close()/show()/showModal(),并捕获异常避免中断渲染。

此外,ReplayerHandler.afterAppend回调会在新插入节点上以后序遍历顺序触发(createdNodeSet弱集合保证顺序与 rrweb-snapshot 包一致,diff.ts),rrweb 回放器用它来通知各插件执行onBuild钩子。

createOrGetNode:虚拟节点 → 真实节点

当 diff 需要插入一个新节点时,调用 diff.ts 的createOrGetNode

  1. 先查 NodeMirror:若该 id 对应的真实节点已存在且类型一致,直接复用;
  2. 否则按虚拟节点类型创建真实节点:SVG 元素走createElementNS(利用SVGTagMapclippath之类的 tagName 还原为 camelCase,并正确设置xlink:href等命名空间,见 diff.ts),普通元素走createElement
  3. 创建后把序列化数据写入 NodeMirror,并登记到createdNodeSet以便触发afterAppend

回放器如何消费 rrdom:useVirtualDom 与 seek 优化

rrdom 真正发挥威力的场景是快进(fast-forward / seek)。在 rrweb 回放器 中:

  1. 回放器持有public virtualDom: RRDocument = new RRDocument()(replay/index.ts);
  2. 默认配置项useVirtualDom: true,即虚拟 DOM 优化默认开启(replay/index.ts),可在playerConfig中关闭(类型定义见 packages/rrweb/src/types.ts);
  3. applyMutation中(replay/index.ts),只有当useVirtualDom 开启、尚未启用虚拟 DOM、且正在同步快进(isSync)三者同时满足时才启动优化,把真实 iframe 文档整树构建为虚拟树buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom)
  4. 快进过程中产生的节点 mutation 只修改虚拟树(成本远低于直接操作真实 DOM),快进结束后一次性调用diff(this.iframe.contentDocument, this.virtualDom, replayerHandler, this.virtualDom.mirror)把累积的差异批量应用到真实 DOM(replay/index.ts),随后destroyTree()复位(index.ts 会清空子节点并mirror.reset())。

源码注释解释了为什么只在「节点类 mutation + 同步快进」时启用:创建虚拟树并执行 diff 的成本通常高于直接应用其他类型的事件(如鼠标、滚动),因此优化只针对最耗时的场景(replay/index.ts)。

回放器把自身能力以ReplayerHandler的形式注入 diff 过程(replay/index.ts 中构造的replayerHandler),包含applyCanvasapplyInputapplyScrollapplyStyleSheetMutationafterAppend,这样 rrdom 只负责结构 diff,而把 canvas 绘制、输入回填、滚动恢复等「副作用」委托给回放器执行——这也是 rrdom 能与 rrweb 解耦的关键设计。

独立安装与使用

rrdom 是发布到 npm 的独立包(当前版本 2.1.5,见 packages/rrdom/package.json),可通过包管理器直接安装:

yarn add rrdom # 或 npm install rrdom

包同时提供 ESM(dist/rrdom.js)、CJS(dist/rrdom.cjs)与 UMD(dist/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 test(vitest)即可运行整个包的全部用例。

小结

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),仅供参考

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

Transformer架构解析:从原理到实践

1. 为什么Transformer彻底改变了AI领域2017年那篇《Attention Is All You Need》论文像一颗炸弹&#xff0c;把传统的RNN和CNN架构炸得粉碎。我在第一次接触Transformer时&#xff0c;被它的并行计算能力震惊了——原来处理序列数据可以不用按部就班地逐个计算。这种架构突破直…

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

CANN进程卡住与进程中断问题定位:2大专题的实战排查方法

CANN进程卡住与进程中断问题定位&#xff1a;2大专题的实战排查方法 【免费下载链接】docs 该仓库用于维护cann公共文档 项目地址: https://gitcode.com/cann/docs 在 CANN&#xff08;华为昇腾 AI 计算架构&#xff09;应用开发中&#xff0c;进程卡住&#xff08;任务长…

作者头像 李华
网站建设 2026/9/21 1:23:37

Python轻量级图书推荐系统:冷启动+长尾优化+双路融合

简介&#xff1a;本资源是一套完整的Python图书推荐系统源码实现&#xff0c;面向高校计算机专业学生、推荐算法初学者及Web开发实践者&#xff0c;聚焦协同过滤与文本相似度融合的推荐策略落地。系统涵盖用户端&#xff08;注册登录、图书浏览/搜索/详情/推荐展示、评论点赞收…

作者头像 李华