Ladybird LibWeb 导航模型解析:执行上下文、Browsing Context 与 Navigable
本文基于 Ladybird 仓库内的 LibWeb: Browsing Contexts and Navigables 设计文档展开,梳理从原生线程执行、ECMAScript 的 Realm/Agent 模型到 HTML 层 Browsing Context 与 Navigable 的完整概念链条。读完本篇,你将理解 Ladybird 浏览器标签页导航在规范概念与 LibWeb 源码中的对应关系,并能沿着 Libraries/LibWeb/HTML/BrowsingContext.h、Libraries/LibWeb/HTML/Navigable.h 等源码继续深入。
一、为什么要先讲"代码如何执行"
原文档开宗明义:在讨论 LibWeb 如何实现 HTML 网页导航之前,必须先厘清规范层面的执行模型——代码到底是如何执行的。它的推理链条是三步走:
- 原生程序(C/C++/Rust)在线程与进程模型上如何执行;
- ECMAScript 规范如何用 Realm、执行上下文、Agent 等概念映射上述模型;
- HTML 规范如何借用这套执行模型,把"网页显示在浏览器标签页里"这件事形式化。
这条链条正是 Ladybird 导航功能的地基:标签页的导航、iframe 的嵌套、跨源隔离,本质上都是对这些规范概念的实现。
二、原生代码执行:执行上下文与符号的两阶段访问
文档"Native Code Execution: A Primer"一节给出了几个关键概念,它们是后续规范讨论的参照系:
- 进程与线程:在"宿主机"环境中,用户态程序的执行通常始于操作系统创建进程。进程包含程序数据与代码的内存空间,以及一个初始的主线程。
- 执行上下文(Execution Context):操作系统要在某个 CPU 核上切换执行的线程时,必须保存并恢复该线程的执行上下文——一组 CPU 寄存器、浮点状态、指向下一条指令的程序计数器(PC)、指向局部数据(函数调用栈与局部变量)的栈指针。程序员也可以通过系统调用请求额外线程,提供线程属性与一个作为入口点的函数。
- 变量与函数的两阶段访问:对传统编译程序而言,"访问变量和函数"被拆成两个阶段。编译期,局部变量与参数被折叠进栈槽或寄存器;导出的变量与函数被写入可执行对象文件(ELF、Mach-O、PE 等),作为符号通过对象格式内的符号表对外可见。通常局部变量名与位置在编译+链接后丢失,除非编译器被配置为输出调试信息。要支持解释型语言那种动态导入,程序员必须调用平台特定函数加载模块(如
dlopen或LoadLibrary);模块打开后,还要逐个符号用另一个平台特定函数(如dlsym或GetProcAddress)取回地址。
这些原生概念在 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 绑定实际上是一个包裹 Window 的 WindowProxy。这与原生可执行文件形成鲜明对比——后者没有代表全局作用域的对象,只有链接器与加载器为各模块提供的符号。
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.html 与 document-sequences.html 章节),包括:
| 成员 | 规范含义 |
|---|---|
m_window_proxy(L98) |
该浏览上下文关联的 WindowProxy,即文档第三节所述"全局 this 实为 WindowProxy"的实体 |
m_window_proxies(L102) |
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_id(L126) |
虚拟浏览上下文组 ID |
m_group(L129) |
指向 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.h 与 BrowsingContextGroup.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_of与top_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 容器内独立导航"的场景,对应源码中的两处证据:
- Libraries/LibWeb/HTML/HTMLIFrameElement.h / HTMLIFrameElement.cpp:iframe 元素作为 NavigableContainer 创建嵌套浏览上下文,即文档第三节所述"单个 Agent 内多个 Realm(iframe 的历史行为)"的实体位置;
- Libraries/LibWeb/HTML/SessionHistoryEntry.h、Libraries/LibWeb/HTML/ApplyHistoryStep.h 与 Libraries/LibWeb/HTML/HistoryOperation.h:历史条目、历史步骤应用与历史记录操作,共同支撑
location.href变更、history.go()等同源/跨源导航流程。
五、文档现状与源码延伸阅读
需要如实说明:Documentation/BrowsingContextsAndNavigables.md 文档自述"work in progress",其"HTML Navigation: Juggling Origins"大节下仍留有 TODO 清单(HTML 规范定义的 Agents 与 Global Objects、Agent 与 Browsing Context Groups、Navigables 关系、标签页/同源与跨源导航走查、嵌套上下文独立导航走查)。本文的第二节至第四节前段继承了文档已完成的叙述,第四节的 BrowsingContextGroup、Navigable 类族与 iframe/历史条目部分,是基于文档 TODO 所指方向、以当前仓库源码为事实依据的补全与印证。
如果你要沿着这条主线继续读码,建议的路径是:
- 从 Libraries/LibWeb/HTML/BrowsingContext.h 出发,对照成员注释中的规范条款理解每个槽位;
- 顺
Navigable→LocalNavigable→LocalTraversableNavigable三级类族理解文档序列模型; - 结合 Libraries/LibWeb/HTML/NavigableContainer.h 与 Libraries/LibWeb/HTML/HTMLIFrameElement.h 看嵌套上下文的创建;
- 再回到 Libraries/LibJS/Runtime/VM.h 与 Libraries/LibJS/Forward.h 查看引擎侧的 realm 与执行上下文基础设施,理解文档第三节中"Host Defined 槽持有 HTML Environment Settings Object"这句话在引擎里的对端。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00