首页
/ Ladybird LibWeb 导航模型解析:执行上下文、Browsing Context 与 Navigable

Ladybird LibWeb 导航模型解析:执行上下文、Browsing Context 与 Navigable

2026-09-05 11:34:28作者:段琳惟

本文基于 Ladybird 仓库内的 LibWeb: Browsing Contexts and Navigables 设计文档展开,梳理从原生线程执行、ECMAScript 的 Realm/Agent 模型到 HTML 层 Browsing Context 与 Navigable 的完整概念链条。读完本篇,你将理解 Ladybird 浏览器标签页导航在规范概念与 LibWeb 源码中的对应关系,并能沿着 Libraries/LibWeb/HTML/BrowsingContext.hLibraries/LibWeb/HTML/Navigable.h 等源码继续深入。

一、为什么要先讲"代码如何执行"

原文档开宗明义:在讨论 LibWeb 如何实现 HTML 网页导航之前,必须先厘清规范层面的执行模型——代码到底是如何执行的。它的推理链条是三步走:

  1. 原生程序(C/C++/Rust)在线程与进程模型上如何执行;
  2. ECMAScript 规范如何用 Realm、执行上下文、Agent 等概念映射上述模型;
  3. HTML 规范如何借用这套执行模型,把"网页显示在浏览器标签页里"这件事形式化。

这条链条正是 Ladybird 导航功能的地基:标签页的导航、iframe 的嵌套、跨源隔离,本质上都是对这些规范概念的实现。

二、原生代码执行:执行上下文与符号的两阶段访问

文档"Native Code Execution: A Primer"一节给出了几个关键概念,它们是后续规范讨论的参照系:

  • 进程与线程:在"宿主机"环境中,用户态程序的执行通常始于操作系统创建进程。进程包含程序数据与代码的内存空间,以及一个初始的主线程。
  • 执行上下文(Execution Context):操作系统要在某个 CPU 核上切换执行的线程时,必须保存并恢复该线程的执行上下文——一组 CPU 寄存器、浮点状态、指向下一条指令的程序计数器(PC)、指向局部数据(函数调用栈与局部变量)的栈指针。程序员也可以通过系统调用请求额外线程,提供线程属性与一个作为入口点的函数。
  • 变量与函数的两阶段访问:对传统编译程序而言,"访问变量和函数"被拆成两个阶段。编译期,局部变量与参数被折叠进栈槽或寄存器;导出的变量与函数被写入可执行对象文件(ELF、Mach-O、PE 等),作为符号通过对象格式内的符号表对外可见。通常局部变量名与位置在编译+链接后丢失,除非编译器被配置为输出调试信息。要支持解释型语言那种动态导入,程序员必须调用平台特定函数加载模块(如 dlopenLoadLibrary);模块打开后,还要逐个符号用另一个平台特定函数(如 dlsymGetProcAddress)取回地址。

这些原生概念在 ECMAScript 规范中几乎都有一一对应的类比。

三、ECMAScript 执行模型:Environment Record、Realm 与执行上下文

3.1 Environment Record:规范层面的命名作用域

文档指出,ECMAScript 规范用"环境记录(Environment Record)"来描述函数、变量与参数的可访问性和作用域范围。要点有两条:

  • 环境记录对执行中的代码不可见,它只是规范作者用来建模语言的一种机制;
  • 每种环境记录包含变量、函数、catch 子句绑定等影响某个位置可见名字的构造,它们以嵌套的树状结构组织,与抽象语法树(AST)大致对应;
  • 树的根是全局环境记录(Global Environment Record),对应全局对象及其属性。

关于全局对象在不同宿主中的形态,文档给出了三种典型场景:REPL 中是 REPL 特有的全局对象(甚至复杂如 Node 或 Bun);浏览器中通常是 Window,除非涉及 Worker;而由于历史原因,Window 上下文中全局 this 绑定实际上是一个包裹 WindowWindowProxy。这与原生可执行文件形成鲜明对比——后者没有代表全局作用域的对象,只有链接器与加载器为各模块提供的符号。

3.2 Realm 与执行上下文:建模"一个概念性执行线程"

全局对象不足以建模 ECMAScript 概念性执行线程的全部状态,规范因此引入 Realm(代码域)执行上下文(Execution Context)

  • Realm 记录是一个容器,持有全局对象、其关联的全局环境、一组内建对象(intrinsic),以及宿主(在部分规范文档中也称 embedder)定义的额外状态。原文档在这一点上给出了 Ladybird 的落地事实:在 LibWeb 中,Host Defined 槽持有每个 realm 的 HTML Environment Settings Object,以及所有需要在全局对象上暴露给 Web API 的原型、构造函数与命名空间
  • 在 Realm 之上,执行上下文建模某一个特定脚本或模块的执行状态。每个执行上下文属于一个执行上下文栈,栈顶称为"运行中执行上下文"。执行上下文承载当前函数、当前代码块所属脚本或模块、访问当前作用域所需的额外环境记录、运行中的生成器状态,以及——对"线程"类比最关键的一点——挂起与恢复该脚本执行所需的状态。
  • 与 Environment Record 一样,Realm 与执行上下文都不能被运行中的 JavaScript 代码直接访问。

在 LibJS 的源码结构中可以印证这一层建模的存在:Libraries/LibJS/Forward.h 前置声明了 class Realm;,而 Libraries/LibJS/Runtime/VM.h 中包含了与执行上下文(execution context)相关的运行时基础设施,说明 realm 与执行上下文的规范概念在引擎层是有对应实体的。

3.3 Agent 与 Agent Cluster:把执行栈调度到真实线程

规范还需要回答:执行上下文栈究竟如何被调度执行?文档的解释是,规范作者不希望用强约束扼杀实现创新,因此引入了两个相关机制——Agent(代理)Agent Cluster(代理簇)

  • 上文提到的执行上下文栈实际上隶属于某个 Agent,Agent 持有该栈、一组关于内存模型的元数据,以及对"执行线程"的共享引用。按 ECMAScript 规范,栈上应始终至少有一个执行上下文,以保证"运行中执行上下文"总能指向周围 Agent 栈顶的执行上下文。但 HTML 规范选择在创建时移除默认执行上下文,改为在脚本、模块与回调执行时手动压栈/出栈。
  • Realm 与 Agent 是 N-1 关系,而非 1-1。在 ECMAScript 规范中这一点体现在 Shadow Realm 提案里;而在 Web 平台上,<iframe> 等元素的历史行为要求单个 Agent 内存在多个 Realm。
  • 文档给出的"松散"理解是:Agent 把一个脚本或模块的执行映射到原生执行线程,同时允许宿主/embedder 自由决定让哪个 Agent 在当前原生线程上运行其执行上下文、以及该 Agent 内哪个 Realm 拥有运行中执行上下文。

SharedArrayBuffer 与 Atomics 给 ECMAScript 带来了类似 C++11、C11 与 Java 5 当年走过的内存模型形式化问题。Agent Cluster 就是把内存模型绑回执行模型的形式化手段:一个 Agent Cluster 是一组能通过共享内存通信的 Agent,机制本身未指定,但硬规则是——同一 Agent Cluster 内的所有 Agent 必须观察到相同的 SharedArrayBuffer 读写顺序及 ECMAScript 原子对象的操作顺序。文档对此的朴素解读是:规范作者设想的正是同一进程内线程间那种内存共享方式,因此 Agent Cluster 松散地建模了在同一实现定义方式下共享内存的一组 Agent(读作线程)。

一句话总结该节结论:Agent 建模某代码模块的"候选执行"(包括该模块及其动态导入中来自生成器、async 等挂起的执行上下文),Agent Cluster 建模具备共享内存通信能力的 Agent 之间的交互

四、HTML 执行模型:全局作用域与 Browsing Context 的落地

文档的"HTML Execution Model: Global Scopes"一节说明:DOM 规范的写法允许为任意语言编写绑定,但经验表明现代浏览器最流行的网页脚本方式是 JavaScript,因此 HTML 规范的脚本 API 是专门为 JavaScript 执行约束裁剪的。规范格外谨慎地保证:不同作者编写的 JavaScript 不能互相干扰,任意脚本不能把页面内容信息外泄到第三方。为此,HTML 规范在 Web 平台脚本行为的开篇就设有"Agents and Agent Clusters"一节(文档中标注该节尚待补完,这属于原文档的 WIP 状态,本文不代为虚构)。

规范概念在 LibWeb 源码中是有明确对应的,以下从源码角度印证文档的核心脉络。

4.1 BrowsingContext:标签页与 iframe 的公共抽象

Libraries/LibWeb/HTML/BrowsingContext.h 中的 Web::HTML::BrowsingContext 继承自 JS::Cell,是受 GC 管理的堆对象,提供两个静态工厂方法:

  • create_a_new_browsing_context_and_document:创建顶层浏览上下文与文档;
  • create_a_new_auxiliary_browsing_context_and_document:以某个 opener 浏览上下文创建辅助(auxiliary)浏览上下文与文档——这正对应 window.open 弹出窗口一类"辅助"场景。

成员变量上的注释直接标注了 HTML 规范条款出处(html.spec.whatwg.org/multipage/browsers.htmldocument-sequences.html 章节),包括:

成员 规范含义
m_window_proxyL98 该浏览上下文关联的 WindowProxy,即文档第三节所述"全局 this 实为 WindowProxy"的实体
m_window_proxiesL102 Bindings::WrapperWorldWeakValueCacheMap<Bindings::WrapperWorld, WindowProxy>,即 WindowProxy 身份按 WrapperWorld 弱缓存:存活的世界 realm 会根定其 global-this WindowProxy,否则代理可能因不携带可观测状态而被回收并重新物化
m_active_document(L105 见后述) 当前激活文档(active document)
m_opener_browsing_context opener 浏览上下文
m_virtual_browsing_context_group_idL126 虚拟浏览上下文组 ID
m_groupL129 指向 BrowsingContextGroup,注释引用 browsers.html 的 #tlbc-group 条款

此外,头文件中的 remove() 方法注释直接引用了规范中"bcg-remove"步骤(L69-L70),is_ancestor_of / is_familiar_with 则支撑文档 TODO 清单里提到的祖先关系与嵌套上下文语义。从源码结构看,is_familiar_with 这类谓词正是 HTML 规范中"两个浏览上下文是否共享一个虚拟浏览上下文组"判定逻辑的实现位点。

4.2 BrowsingContextGroup:与 Agent 平行的组模型

Libraries/LibWeb/HTML/BrowsingContextGroup.hBrowsingContextGroup.cpp 实现了文档 TODO 清单中"Agents and Browsing Context Groups"一节指向的实体:HTML 规范中的浏览上下文组(Browsing Context Group)在职责上与 Agent 平行,用于隔离不同组的脚本执行与源(origin)判定。BrowsingContext::group() / set_group()L65-L67)维护了上下文与组的双向关联,virtual_browsing_context_group_id()L61)则为跨进程场景保留了规范要求的组标识。

4.3 Navigable 与 Traversable Navigable:文档序列层

文档 TODO 清单列出的"Navigables 与 Browsing Contexts 的关系""标签页及其 traversable navigable 的构造"在 LibWeb 中对应一套清晰的类族。Libraries/LibWeb/HTML/Navigable.h 定义了抽象基类 Navigable(同样继承 JS::Cell):

  • id() 返回 CrossProcessId 类型的规范"nav id"(L26-L27),类型定义于 Libraries/LibWeb/HTML/CrossProcessId.h,说明该 ID 被设计为可跨进程引用——这与 Ladybird 多进程架构中 Navigable 可能位于 WebContent 进程之外的情况一致(从源码结构看);
  • parent()L29)维护嵌套关系,is_ancestor_oftop_level_traversable()L31-L35)支持从任意嵌套 navigable 上溯到顶层 traversable;
  • 纯虚方法 active_window_proxy()target_name()active_document_url()active_document_origin()L33-L37)把"当前窗口代理、目标名、激活文档的 URL 与源"抽象为接口,供导航流程统一使用。

具体实现分为两级:LocalNavigable.h 表示位于本进程内的 navigable,LocalTraversableNavigable.h 进一步表示"可遍历"的本地 navigable——traversable 意味着它拥有一个浏览上下文树、可以承载导航会话与历史记录。BrowsingContext::top_level_traversable()BrowsingContext.h L36)返回 GC::Ref<LocalTraversableNavigable>,正是文档 TODO 中"标签页的 traversable navigable"的取用入口。此外 NavigableContainer.h 定义了持有 navigable 的容器(如 iframe 元素),把"元素—navigable—浏览上下文—文档"的链路接起来。

4.4 嵌套浏览上下文与导航流程的落点

文档 TODO 中"嵌套浏览上下文在其 navigable 容器内独立导航"的场景,对应源码中的两处证据:

五、文档现状与源码延伸阅读

需要如实说明:Documentation/BrowsingContextsAndNavigables.md 文档自述"work in progress",其"HTML Navigation: Juggling Origins"大节下仍留有 TODO 清单(HTML 规范定义的 Agents 与 Global Objects、Agent 与 Browsing Context Groups、Navigables 关系、标签页/同源与跨源导航走查、嵌套上下文独立导航走查)。本文的第二节至第四节前段继承了文档已完成的叙述,第四节的 BrowsingContextGroup、Navigable 类族与 iframe/历史条目部分,是基于文档 TODO 所指方向、以当前仓库源码为事实依据的补全与印证。

如果你要沿着这条主线继续读码,建议的路径是:

  1. Libraries/LibWeb/HTML/BrowsingContext.h 出发,对照成员注释中的规范条款理解每个槽位;
  2. NavigableLocalNavigableLocalTraversableNavigable 三级类族理解文档序列模型;
  3. 结合 Libraries/LibWeb/HTML/NavigableContainer.hLibraries/LibWeb/HTML/HTMLIFrameElement.h 看嵌套上下文的创建;
  4. 再回到 Libraries/LibJS/Runtime/VM.hLibraries/LibJS/Forward.h 查看引擎侧的 realm 与执行上下文基础设施,理解文档第三节中"Host Defined 槽持有 HTML Environment Settings Object"这句话在引擎里的对端。
登录后查看全文
热门项目推荐
相关项目推荐