原文地址。
( 译者注:这是一篇深度好文,并且附带官方简体中文。本次的翻译为一人完成,限于水平,难免有误,故需要学习本文内容的同学请直接参考原文地址进行阅读。
导读: 终于,我在一周之内讲这长篇大论的浏览器背后的故事翻译完了。如果再要我重新阅读一遍,可能需要我沐浴焚香般的准备。现在我忍着肩膀和手腕的酸痛,写下发布前的最后一些体会:
- 这是一篇长度不小的文章。但是整个文章的内容可以说已经十分精炼,是一个以色列开发者在查阅数百万行 c++ 代码和无数文档的凝结之作。希望想更深入了解浏览器内部的读者们收藏原文。
- 尽管翻译结束了,但是我仍然需要好好消化一下文章内容。本来想挑出一部分关键内容供大家参考。但实在难以取舍。所以想要了解文章内容的同学,请快速阅览目录进行检索。
- 这篇文章,应该是我近一个月以来最长的一篇了。手酸,就写到这里吧。 )
序言
这是篇全面介绍 WebKit 和 Gecko 的内部操作的文章,它是以色列的开发者 Tail Garsiel 的大量的研究成果。过去几年,她重新审视了已公开的关于浏览器内部的资料(参考资料)同时花费了很多时间去阅读 web 浏览器源码。她写道:
在 IE 90% 支配的那个年代,把浏览器当做一个“黑盒”再也合适不过了,但是现在,开源浏览器占据了一半的市场份额,是时候去了解引擎的背后同时看看 web 浏览器的内部。尽管,里面有数百万行 C++ 代码……
Tail 在她的网站上发布了她的研究,但是我们想让更多的人知道,所以我们已经重新整理再次发布在这里。
作为一个 web 开发,了解浏览器操作的内部会帮助你做出更好的决定,同时在最佳开发实践时了解充分的理由。而这是一篇有长度的文章,我们推荐你花点时间去深挖探究,我们保证你会对自己的所做满意。 Paul lrish, Chrome 开发人员关系部
这篇文章被翻译成几种语言:HTML5 Rocks 翻译了 德语,西班牙语,日语,葡萄牙语,俄语和简体中文版本。你也可以看到韩语和土耳其语。 你也能看看关于这篇主题 Tail Garsiel 在 Vimeo 上的谈话。
(译者注:这篇目录翻译了我半个小时,通过目录的回顾确实跟之前的一些零碎知识串联了起来,发现很多。更主要的是,跑个题来缓解下被这个目录吓尿的心脏。)
目录
Web 浏览器是使用最广泛的软件。这篇读物中,我会解释在场景之后他们是如何工作的。我们将会看到,当你在地址栏输入 google.com 时直到在浏览器屏幕上看到 Google Page 页面后,发生了什么。
1.介绍
- 目录
- 1.介绍
- 1.我们将要讨论的浏览器
- 2.浏览器的主要功能
- 3.浏览器的上层结构
- 2.渲染引擎
- 1.渲染引擎
- 2.主要过程
- 3.主要过程例子
- 3.解析和 DOM 树结构
- 1.一般解析
- 1.语法
- 2.解析器——词法分析器混合
- 3.翻译
- 4.解析举例
- 5.变量和语法的定义格式
- 6.解释器类型
- 7.自动生成解析
- 2.HTML 解析
- 1.HTML 语法定义
- 2.不是上下文无关文法
- 3.HTML DTD
- 4.DOM
- 5.解析算法
- 6.断词(标记)算法
- 7.树构造器算法
- 8.解析结束的行为
- 9.浏览器容错度
替换- 交叉表格
- 嵌套元素
- 深度标签层级
- 错误放置的 html 或 body 结束标签
- 3.CSS 解析
- 1.WebKit CSS 解析
- 4.脚本和样式表的执行顺序
- 1.脚本
- 2.推断解析
- 3.样式表
- 1.一般解析
- 4.渲染树构造器
- 1.渲染树和 DOM 树的关系
- 2.树构建的过程
- 3.样式计算
- 1.共享样式数据
- 2.火狐规则树
- 1.结构分隔
- 2.使用规则树计算样式上下文
- 3.为简单匹配控制规则
- 4.在正确的层叠顺序中应用规则
- 1.样式表层叠规则
- 2.明确性
- 3.规则排序
- 4.逐步过程
- 5.布局
- 1.Dirty 位系统
- 2.全局和增量布局
- 3.异步和同步布局
- 4.优化
- 5.布局过程
- 6.宽度计算
- 7.断行
- 6.绘制
- 1.全局和增量
- 2.绘制顺序
- 3.Firefox 展示列表
- 4.WebKit 矩阵存储
- 7.动态改变
- 8.渲染引擎的线程
- 1.事件循环
- 9.CSS2 可视模型
- 1.canvas
- 2.CSS 盒模型
- 3.定位方案
- 4.盒子类型
- 5.定位
- 1.相对
- 2.浮动
- 3.绝对和固定
- 6.表现层
- 10.资料
- 1.浏览器结构
- 2.解析
- 3.火狐
- 4.WebKit
- 5. W3C 规范
- 6. 浏览器构建说明
- 1.介绍
1.我们将要讨论的浏览器
如今常用的主要浏览器有 5 种: Chrome,IE,火狐,Safari 和 Opera。在移动端上,主要的浏览器是安卓浏览器,苹果,Opera 迷你和 Opera移动端还有 UC 浏览器,诺基亚 S40/S60 浏览器和 Chrome 也都是,除了 Opera 浏览器,其他都是基于 WebKit。(译者注:前一句话在官方简体中文里没有.)我从开源浏览器火狐和 Chrome 以及 Safari(部分开源)中距离。根据 StatCounter 统计(从2013年6月以来) Chrome 火狐和 Safari 组成了全球桌面浏览器使用量的 71%。在移动端,安卓浏览器,iPhone 和 Chrome 有 54% 的使用率。
2.浏览器的主要功能
浏览器的主要功能是展示你选择的 web 资源,通过服务端的请求然后在浏览器窗口展示。这个资源通常是一个 HTML 文档,但也有可能是一个 PDF,一张图片,或者其他类型的内容。资源的位置通过用户使用 URI(Uniform Resource Identifier) 来明确指出。
浏览器插入和展示 HTML 文件的方式在 HTML 和 CSS 规范中有详细说明。这些规范通过 W3C(World Wide Web Consortium) 维护,这些规范也是 web 的标准组织。这些年的浏览器只是遵守一部分标准同时开发了他们自己的扩展。这对 web 开发者来说引发了一系列的兼容性问题。如今大多数浏览器或多或少遵守这些规范。
浏览器用户界面相互有很多共同之处。它们之间的共同元素有:
- 用于插入 URL 的地址栏
- 前进和后退按钮
- 书签选择
- 用于刷新和停止加载当前文档的刷新和停止按钮
- 带你去主页的主页按钮
奇怪的是,浏览器的用户界面没有任何形式的规范,它只是从过去几年的经验和通过浏览器相互模仿中产生的最佳实践。 HTML5 规范没有定义一个浏览器必须拥有的 UI 元素,但是列出了一些常见元素。它们有地址栏,状态栏和工具栏。这些内容,尤其是,像火狐的下载管理器对具体浏览器而言是特有的。
3.浏览器的上层结构
浏览器的主要组件有(1.1):
- 用户界面:这包括地址栏,前进/后退按钮,书签菜单等等。每个部分除了你请求页面的的窗口都会显示。
- 浏览器引擎:在 UI 和渲染引擎之间的统一行为
- 渲染引擎:对展示请求的内容响应。比如请求的内容是 HTML,这个渲染引擎会解析 HTML 和 CSS,同时在屏幕上展示解析后的内容。
- 网络:对于网络调用比如 HTTP 请求,在独立的平台接口下对不同的平台使用不同的实现。
- UI 后台:用于绘制像组合盒子和窗口的基本组件。这个后台暴露的通用接口不是平台特有的。在这之下它使用了操作系统用户界面的方法。
- JavaScript 解释器:用于解释和执行 JavaScript 代码
- 数据存储:这是持续存在的一层。浏览器可能需要本地化存储数据的顺序,比如 cookies。刘安琪也支持 storage 机制比如 LocalStorage,IndexDB,WebSQL 和 文件系统。
对于浏览器而言这是很重要的,比如 Chrome 运行多个渲染引擎的实例为每一个标签。每个标签有个独立的进程。
2.渲染引擎
渲染引擎的责任是,额……渲染,也就是在浏览器屏幕上展示请求的内容。
默认的渲染引擎可以展示 HTML 和 XML 文档以及图片。它也可以通过插件或者扩展来展示其他的数据类型。举个例子,使用 PDF 视图插件展示 PDF 文档。然而,在本章节中我们将关注它的主要用处:展示使用 CSS 格式化的 HTML 和 图片。
1.渲染引擎
不同的浏览器使用不同的渲染引擎:IE 使用 Trident,火狐使用 Gecko,Safari 使用 WebKit。Chrome 和 Opera(自 15 版)使用 Blink,是Webkit 的一个分支。
WebKit是一个开源引擎,作为引擎,最开始在 Linux 平台上然后被 Apple 为了支持 Mac 和 Windows而修改。了解webkit.org的更多细节。
2.主要过程
渲染引擎从网络层开始获取请求文档的内容。这个通常在一个 8KB 块中完成。
在那之后,展示了渲染引擎的基本流程:
渲染引擎开始解析 HTMl 文档同时在一个名叫“内容树”的树中转化元素变成 DOM 节点。引擎将会解析样式数据,外部的 CSS 文件和元素样式。在 HTML 中带有可视指令的样式信息将会被用于创建另一个树:渲染树。
渲染树包含了有可视属性像是颜色和尺寸的矩形。这个矩形在屏幕上以正确的顺序展示。
之后的渲染树会通过一个“布局”进程。这意味着该进程会给在屏幕上应该出现的每个节点一个精确的坐标。下一个阶段是绘制——渲染树被转换然后每个节点通过 UI 后台层被绘制。
理解这个渐进过程是非常必要的。为了更好地用户体验,渲染引擎会尽快尝试在屏幕上展示内容。它在开始构建和布局渲染树之前,不会等待所有的 HTMl 被解析。一部分内容被解析和展示,而进程继续剩余的从网络来的内容。
3.主要过程例子
3.6
从图 3 和图 4你可以看到,WebKit 和 Gecko 术语上有点不同,过程还是基本一样的。
Gecko 调用一个树,这个树是可视化的一个被格式化的“框架树”。每个元素是一个框架。WebKit 使用的叫做“Render Tree”,同时它由“Render Objects”组成。WebKit 对元素位置使用“Layout”,而 Gecko 称它为 “Reflow”。“Attachment”是WebKit一个术语,用于连接 DOM 节点和可视化信息用于创建渲染树。一个不重要的非语义的不同是 Gecko 在 HTML 和 DOM 树之间有一个额外的层,叫做 “content sink(内容沉淀)”,同时它是一个制作 DOM 元素的工场。我们将会讨论过程的每个部分:
3.解析和 DOM 树结构
1.一般解析
因为在渲染引擎中,解析是非常明显的进程,我们将会探索的深入一点。通过关于解析的简单介绍来开始。
解析一个文档意味着翻译为代码可用的结构。解析的结果通常是一个节点树,这颗树代表着文档结构。通常叫做解析树或者语法树。
举个例子,解析表达式 2 + 3 -1
可以返回下面的树:
1.语法
解析基于文件遵守的语法规则:语言或者写入的格式。所有可以解析的格式必须由词汇和句法规则构成确定的语法。这称为上下文无关语法。人类语言不是这种语言,因此不能用常规的解析技术解析。
2.解析器——词法分析器混合
解析可以分为两个独立的子过程:词法分析和语法分析。
词法分析是将输入变成为标记的过程。标记是语言词汇:构建块的集合。在人类语言中它由所有在这个语言的字典中出现的单词构成。
语法分析是语言语法规则的应用。
解析通常在两个部分中独立的工作:词法分析器(有时也叫标记分析器),负责将输入变成有效地标记,同时解析器的责任是通过根据语言规则来分析文档构建解析树。词法分析器知道如何去除不相关的字符比如空格和换行。
解析过程是反复的。解析器通常为新的标记向词法分析器请求,并尝试将标记与某条语法规则匹配。如果规则匹配了,一个相应标记的节点将被添加到语法树中去,并且解析会请求下一个标记。
如果没有规则匹配,解析器会内部储存这个标记,并且保持请求标记直到一个匹配到所有储存在内部的标记的规则被发现。如果没有找到规则,那么解析器将抛出一个错误。这意味着文件无效并且包含语法错误。
3.翻译
在很多例子中,解析树不是最终产物。解析通常用于翻译:转换输入文档为另一种格式。举个例子比如编译:编译器编译源码成为机器码,首先编译成编译树,然后再编译成机器码文件。
4.解析举例
在图表5中,我们从数学表达式中构建编译树。我们试试定义一个简单的数学语言然后看看编译过程。
词汇:我们的语言包括数字,加减号。 语法:
- 语言语法组成了表达式,项和操作符。
- 语言包括任何数字表达式。
- 作为项的表达式通过链接另一个项的“操作符”链接。
- 操作符是加号或者减号标记。
- 项是数字或者表达式。
我们来分析输入的: 2 + 3 - 1
首先匹配到的规则串是 2
:根据规则 #5 这是一个项。第二个匹配是 2 + 3
:这个匹配了第三个规则:一个项链接着一个链接另一个项的操作符。下一个匹配将在输入的最后。2 + 3 -1
是一个表达式,因为我们知道, 2 + 3
是一个项,所以我们有一个项,通过链接另一个项的操作符链接着。2 + +
不会匹配任何规则,因此是一个无效的输入。
5.变量和语法的定义格式
词汇通常通过正则表达式表现。
举个例子,我们的语言将被定义为如下:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
复制代码
正如你看到的,整型通过正则表达式定义。
语法通常通过一种叫做 BNF 的格式定义。我们的语言将被定义为如下:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
复制代码
我们认为,如果一种语言的语法是上下文无关的,那么它可以通过常规解析器解析。上下文无关语法的直观定义是可以在 BNF 中被完全表达的语法。一种形式的定义是关于上下文无关文法的维基百科的文章。
6.解释器类型
这里有两种解析类型:自顶向下和自底向上的解析。直观的解释是,自顶向下的解析检查上层语法结构然后尝试找到规则匹配。自底向上从输入开始,逐级转化成语法规则,从底层规则开始直到遇到顶层规则。
我们看看这两种解析类型如何解析我们的例子。
自顶向下解析从上层开始:它将识别 2 + 3
为一个表达式。然后识别 2 + 3 - 1
为一个表达式(识别表达式的过程是逐步的,匹配其他规则的,但是是从上层开始。)
自底向上解析将会浏览输入直到一个规则被匹配。它将用这个规则替换匹配。这会一直执行直到输入的末端。部分匹配到的表达式在解析栈上被替换。
Stack | Input |
---|---|
term | 2 + 3 -1 |
term operation | + 3 - 1 |
expression | 3 - 1 |
expression operation | - 1 |
expression | - |
这种自底向上的解析称为移入规约解析,因为输入偏移到右侧(想象一个指针在输入开始然后移动到右侧),并且逐渐减少语法规则。
7.自动生成解析
有个工具可以生成解析。你告诉工具你的语言语法——它的词汇和语法规则——然后工具会生成一个有效解析。创建解析需要深刻理解解析,并且手动创建一个优化的解析并不容易,所以解析生成器是很有用的。
WebKit 使用两个著名的解析工具: Flex,用于创建词法,Bison,用于创建解析(你会或许把他们称为 Lex 和 Yacc)。Flex 输入是一个文件,包含标记的正则表达式定义。Bison 的输入是在 BNF 格式中的语言与法。
2.HTML 解析
HTML 解析器的工作是把 HTML 标记转化成解析树。
1.HTML 语法定义
HTML 的词汇和语法由 W3C 组织创造,在这个规范被定义。
2.不是上下文无关文法
如同我们在解析中的介绍,语法可以像 BNF 那样使用格式化的格式被定义。
不幸的是,所有常规的解析方式不能应用于 HTML(为了好玩我不把它们现在引入——它们在解析 CSS 和 JavaScript时将被用到)。HTML 不能像解析器需要的那样通过上下文无关文法被定义。
乍一看这个表现很奇怪; HTML 十分像 XML。有非常多的 XML 解析器可以使用。HTML 是 XML 的变体——所以有什么很大的不同吗?
这里的不同在于,HTML 尽可能的更多“包容”:它能让你省略某些标签(那些被隐式添加的),或者有时候省略开始或结束标签等等。整体来看它是“软性语法,与 XML 的严格硬性语法不同。
3.HTML DTD
HTML 定义在一种 DTD 格式里。这种格式用于定义 SGML 家族的语言。这个格式为所有允许的元素,它们的属性和等级定义。我们之前看到,HTML DTD 不是一种上下文无关文法。
DTD 有一些变体。严格模式适用于唯一的规范,但是其他模式包含对过去浏览器使用的标记的支持。这个目的是可以向后兼容老旧的内容。目前严格模式的 DTD 参考这里www.w3.org/TR/html4/st…。
4.DOM
输出树(解析树)是 DOM 元素和节点属性的树。DOM 是 Document Object Model 的缩写。它是 HTML 文档的表现对象和 HTML 元素对外部世界像是 JavaScript 元素的接口。树的根节点是 “Document” 对象。
DOM 对标记来说几乎要一对一的关系。举个例子:
<html>
<body>
<p>
Hello World
p>
<div> <img src="example.png"/>div>
body>
html>
复制代码
标记将会被转化为下面的 DOM 树:
像 HTML,DOM 通过 W3C 组织定义。参考这里www.w3.org/DOM/DOMTR。它是对操作文档的一般定义。特殊的模型描述了 HTML 特殊元素。HTML 定义可以在这里找到:www.w3.org/TR/2003/REC…。
当我谈到树包含 DOM 节点时,我的意思是这棵树是有结构的元素,实现了 DOM 其中之一的接口。浏览器混合了这些实现,这些实现有一些通过浏览器内部定义的其他属性。
5.解析算法
如我们之前看到的部分一样,HTML 不能使用常规的自顶向下或者自底向上解析。
原因有:
- 语言的包容性
- 事实是,浏览器有错误容忍的传统,为了支持常见的无效的 HTML 的情况。
- 解析过程是不断重复的。对于其他语言,源码在解析的时候不会改变,但是 HTML,动态代码(比如包含
document.write()
的脚本元素调用)可以添加额外的标记,所以解析过程实际上修改了输入。
不能使用常规解析技术,浏览器为解析 HTML 创建了自定义解析。
由 HTML5 规范定义了解析算法的细节。算法有两个阶段组成:标记(断词)和结构树。
标记是词法分析,解析是输入变成标记。在 HTML 中,标记是开始标签,结束标签,属性名和属性值。
标记器识别标记,把标记给树构造器,并且为下个识别的标记处理下个字符,直到输入的结尾。
6.断词(标记)算法
这个算法的输出是 HTML 标记。这个算法被作为状态机表达。每个状态使用一个或者多个输入流的字符,并且根据这些字符更新下一个状态。这个决定通过当前标记状态和树构造状态影响。这就意味着消耗同样的字符为了正确的下个状态将会产出不同的结果,这取决于当前状态。这个算法过于复杂,以致不能完全描述,我们来看看一个简单的例子,这可以帮助我们理解这个规则。
基本例子:标记以下 HTML:
<html>
<body>
Hello world
body>
html>
复制代码
初始化状态是 “Data State”。当遇到 <
字符时,状态变成“Tag open state”。使用 a-z
的字符产生“Start tag token”的创建,状态变为“Tag name state”。我们保留这个状态直到 >
字符出现。每个字符都被添加到新的标记名称上。在我们的例子中,这个创建的标记是 html
标记。
当 >
标签出现,当前标记被发送,同时状态变回 Data state
。 标签也是用相同的步骤处理。目前为止,
html
和 body
标签被发送了。我们现在回到了 “Data state”。遇到 Hello world
字符的 H
将会引起创建和字符标记的发送,这将一直进行直到遇见 的
<
。我们将为 Hello world
的每一个字符发送一个字符标记。
现在我们回到“Tag open state”。遇到下一个输入 /
将会引起结束标签的创建,并且移动到“Tag name state”。再一次我们保持在这个状态,直到我们遇见 >
。此时这个新的标签标记将被发送,并且我们回到“Data state”。