How browsers work(zh-CN)【Tr.001】
浏览器如何运作
现代web浏览器的背后
序
这份关于WebKit和Gecko内部操作的全面入门材料,是以色列开发人员Tali Garsiel所做的大量研究的结果。在过去的几年里,她审阅了所有关于浏览器内部结构的公开数据,并花了大量时间阅读网络浏览器的源代码。她写道:
Tali在她的网站上发表了她的研究,但我们知道这值得被广泛传播,所以我们把它整理了一下,并在这里重新发表。
作为一名web开发人员,了解浏览器内部的操作原理可以帮助你做出更好的决策,并了解最佳开发方法背后的理由。虽然这是一个相当长的文档,但我们建议您花些时间深入研究。我们保证你研究完之后会很满意。
Paul Irish, Chrome Developer Relations
简介
web浏览器是使用最为广泛的软件。在本文中,我将解释它们在幕后是如何工作的。从你在地址栏中输入google.com
开始,直到浏览器屏幕上显示谷歌页面为止,浏览器到底干了些什么?让我们一起来探究这个过程。
我们将要讨论的浏览器
现在有五种主要的桌面浏览器:Chrome, Internet Explorer, Firefox, Safari和Opera。在移动端,主要的浏览器有Android浏览器、iPhone、Opera Mini和Opera mobile、UC浏览器、Nokia S40/S60浏览器和Chrome。在这其中,除了Opera浏览器,其他浏览器都基于WebKit。我将从开源浏览器Firefox和Chrome以及Safari(部分开源)中举例。根据StatCounter的统计数据(截至2013年6月),Chrome、Firefox和Safari占据了全球桌面浏览器使用量的71%左右。在移动设备上,Android浏览器、iPhone和Chrome的使用率约为54%。
浏览器的主要功能
浏览器的主要功能是,从服务器请求所选择的web资源,并在浏览器窗口中显示它们。这些资源通常是HTML文档,但也可能是PDF、图像或其他类型的内容。资源的位置由用户使用URI(Uniform Resource Identifier统一资源标识符)指定。
浏览器解释和显示HTML文件的方式由HTML和CSS的规范指定。这些规范由W3C(World Wide Web Consortium万维网联盟)组织维护,该组织是web的标准组织。多年来,浏览器只遵循了部分规范,并开发了自己的扩展。这给网页作者带来了严重的兼容性问题。如今,大多数浏览器或多或少都符合规范。
浏览器用户界面之间有很多共同之处。常见的用户界面元素有:
- 用于插入URI的地址栏
- 后退和前进按钮
- 书签选项
- 刷新和停止按钮,用于刷新或停止对当前文档的加载
- 返回主页的Home按钮
奇怪的是,没有任何正式的规范指定了浏览器的用户界面。这只是来自于多年的经验和浏览器相互模仿而形成的良好结果。HTML5规范没有定义浏览器必须拥有的UI(User Interface,用户界面)元素,但还是列出了一些常见元素。其中包括地址栏、状态栏和工具栏。当然,某些浏览器也有其独特的功能,比如Firefox的下载管理器。
浏览器的宏观结构
浏览器的主要组成部分有:
- 用户界面:包括地址栏、后退/前进按钮、书签菜单等。这包含浏览器显示的每个部分,除了您查看请求页面的窗口。
- 浏览器引擎:整合渲染引擎并构建UI。
- 渲染引擎:负责显示请求的内容。例如,如果请求的内容是HTML页面,呈现引擎将解析HTML和CSS,并在屏幕上显示解析后的内容。
- 网络:对于网络调用(如HTTP请求),在与平台无关的接口后面使用针对不同平台的不同实现。
- UI后端:用于绘制基本的小部件,如组件(combo box)和窗口。这个后端暴露了一个平台通用的接口。它向下直接调用操作系统UI相关的方法。
- JavaScript解释器:用于解析和执行JavaScript代码。
- 数据存储:这是一个持久层。浏览器可能需要在本地保存各种数据,比如cookie。浏览器还支持localStorage、IndexedDB、WebSQL和文件系统等存储机制。
需要注意的是,浏览器(如Chrome)会运行多个渲染引擎实例:每个页面一个实例。每个页面在单独的进程中运行。
渲染引擎
渲染引擎的职责嘛…就是渲染,即在浏览器屏幕上显示所请求的内容。
默认情况下,渲染引擎可以显示HTML和XML文档和图像。它可以通过安装插件或扩展来显示其他类型的数据。例如,使用PDF查看器插件显示PDF文档。然而,在本章中,我们将关注主要的用例:显示使用CSS格式化的HTML和图像。
多种渲染引擎
不同的浏览器使用不同的渲染引擎:Internet Explorer使用Trident, Firefox使用Gecko, Safari使用WebKit。Chrome和Opera(从版本15开始)使用Blink,这是WebKit的一个分支。
WebKit是一个开源的渲染引擎,最初用于Linux平台,后来被苹果修改为支持Mac和Windows。详情请参见webkit.org。
渲染的主要流程
渲染引擎将开始从网络层获取所请求文档的内容。这些内容通常被分为很多8kB的块。
以下是渲染引擎的基本流程:
渲染引擎将开始解析HTML文档,并将元素转换为称为“content tree”的树中的DOM节点。引擎将解析外部CSS文件和样式元素中的样式数据。样式信息和HTML中的可视指令将用于创建另一棵树:渲染树。
渲染树包含具有颜色和尺寸等视觉属性的矩形渲染区域。这些矩形以正确的次序显示在屏幕上。
在构建渲染树之后,它会经历一个“布局”过程。这意味着为树中每个节点提供它应该出现在屏幕上的确切坐标。下一个阶段是绘制——渲染树将被遍历,每个节点将使用UI后端层绘制。
重要的是要明白,这是一个渐进的过程。为了更好的用户体验,渲染引擎会尽量在屏幕上尽快显示内容。它不会等到所有HTML都解析完毕后才开始构建和布局渲染树。部分内容将提前被解析和显示,而随着来自网络的其余内容到达,该过渲染过程将继续。
主要流程示例
Mozilla 的 Gecko 渲染引擎从图3和图4可以看到,尽管WebKit和Gecko使用的术语略有不同,但流程基本上是相同的。
Gecko称视觉上格式化的元素树为“Frame tree”。每个元素都是一个Frame。WebKit使用术语“Render Tree”,它由“Render Objects”组成。WebKit使用术语“Layout”来放置元素,而Gecko称之为“Reflow”。“Attachment”是WebKit用于连接DOM节点和可视信息以创建渲染树的术语。一个微小的非语义差异是,Gecko在HTML和DOM树之间有一个额外的层。它被称为“Content Sink”(内容槽),是制作DOM元素的工厂。我们将讨论流程的每个部分:
解析-简介
由于解析在呈现引擎中是一个非常重要的过程,我们需要更深入地讨论它。让我们先简单介绍一下解析。
解析文档意味着将其转换为程序可以使用的结构。解析的结果通常是表示文档结构的节点树。这被称为解析树(parse tree)或语法树。
例如,解析表达式2 + 3 - 1
可以返回这样的树:
语法
解析过程基于文档的句法(syntax,语言中单词或语句的排列方式)。文档遵循的句法,指的是编写文档的语言或格式。每种可以解析的格式都必须具有确定的语法(grammar,语言的整体结构和用法),一般由词汇和句法规则组成。这被称为上下文无关语法。人类语言不是这样的语言,因此不能用传统的解析技术进行解析。
解析器-词法分析器(Lexer)的组合
解析过程可以分为两个子过程:词法分析(lexical analysis)和句法分析。
词法分析是将程序输入分解为标记(Token)的过程。标记是语言的词汇,即有效构组成成分的集合。在人类语言中,它由词典中出现的该语言的所有单词组成。
句法分析是运用语言句法的规则。
解析器通常将任务划分为两个部分:词法分析器(有时称为标记器,tokenizer)负责将输入分解为有效的标记;解析器负责根据句法规则分析文档结构,来构造解析树。
词法分析器知道如何去除不相关的字符,如空格和换行符。
解析过程是迭代的。解析器通常会向词法分析器请求一个新的标记,并尝试将该标记与语法规则之一匹配。如果匹配到规则,则与该标记对应的节点将被添加到解析树中,解析器将请求另一个标记。
如果没有匹配的规则,解析器将在内部存储标记,并继续请求标记,直到找到与所有内部存储的标记匹配的规则。如果没有找到规则,解析器将抛出异常。这意味着文档无效并且包含句法错误。
翻译
在许多情况下,解析树并不是最终产物。解析通常用于翻译:将输入文档转换为另一种格式。一个较好的例子就是“编译”。编译器能将源代码编译为机器码。它首先将其解析为解析树,然后将树转换为机器码文档。
解析过程的例子
在图5中,我们根据一个数学表达式构建了解析树。让我们尝试定义一种简单的数学语言,并观察解析表达式的过程。
关键术语:我们的语言可以包括整数、加号和减号。
句法:
- 这个语言中,句法的基本单元是“表达式”、“项”和“操作”
- 我们的语言可以包含任意数量的“表达式”
- “表达式”定义为“项”后面跟着“操作”,后面跟着另一个“项”
- 一个“操作”是一个“加号”或“减号”
- “项”是一个“整数”或“表达式”
我们分析一下输入2 + 3 - 1
。
配规则的第一个子字符串是2
:根据规则#5,它是一个项。第二个匹配是2 + 3
:它匹配规则#3:一个项后面跟着一个操作,后面跟着另一个项。下一个匹配只会在输入的末尾被确认。2 + 3 - 1
是一个表达式,因为我们已经知道2 + 3
是一个项,所以我们有一个项,后面跟着一个运算,后面跟着另一个项。2 + +
不会匹配任何规则,因此是无效输入。
词汇和句法的正式定义
词汇表通常由正则表达式表示。
例如,将我们语言词汇定义为:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
如您所见,整数是由正则表达式定义的。
句法通常以一种称为BNF(Backus–Naur form,逆波兰表达式)的格式定义。我们的语言被定义为:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression -
我们说过,如果一种语言的语法是与上下文无关的语法,那么它就可以被常规解析器解析。上下文无关语法的直观定义是,可以完全用BNF表示的语法。有关BNF的正式定义,请参阅维基百科的文章:上下文无关语法
解析器的类型
有两种类型的解析器:自顶向下解析器和自底向上解析器。一种直观的解释是,自顶向下解析器检查语法的高级结构,并试图找到匹配的规则。自底向上解析器从输入开始,逐步将其转换为语法规则,从低级规则开始,直到满足高级规则。
让我们看看这两种类型的解析器将如何解析我们的示例。
自顶向下解析器将从更高级别的规则开始:它将把2 + 3
标识为表达式。然后,它将2 + 3 - 1
标识为表达式(识别表达式的过程不断推进,匹配其他规则,但起点是最高级别的规则)。
自底向上解析器将按一个方向扫描输入,直到匹配到规则。然后它将用规则替换匹配的输入。这将一直持续到输入的结束。部分匹配的表达式放在解析器的堆栈上。
栈 | 输入 |
---|---|
2 + 3 - 1 | |
项 | + 3 - 1 |
项 操作符 | 3 - 1 |
表达式 | - 1 |
表达式 操作符 | 1 |
表达式 |
这种类型的自底向上解析器称为shift-reduce解析器,因为输入是向右移动的(想象一个指针首先指向输入起点并向右移动),并且逐渐被简化为句法规则。
自动生成解析器
有一些工具可以生成解析器。你向它们提供你的语言的语法——词汇和语法规则——它们就会生成一个可以工作的解析器。解析器生成器可能非常有用,因为手动创建并优化出一个解析器并不容易。手动创建解析器需要非常深入地理解解析过程。
WebKit使用了两个著名的解析器生成器:用于创建词法分析器的Flex,和用于创建解析器的Bison(您可能会在它们中遇到Lex和Yacc这两个名称)。Flex的输入是一个包含标记的正则表达式定义的文件。Bison的输入是BNF格式的语言语法规则。
HTML 解析器
HTML解析器的任务是将HTML标记解析为解析树。
HTML语法定义
HTML的词汇和语法在W3C组织创建的规范中定义。
不是上下文无关的语法
正如我们在解析的介绍中所看到的,语法的句法可以用像BNF这样的格式正式定义。
不幸的是,所有传统的解析器主题都不适用于HTML(我提出它们并不是为了好玩——它们将用于解析CSS和JavaScript)。HTML不能轻易地用解析器需要的上下文无关语法来定义。
定义HTML有一个正式的格式——DTD(Document Type Definition,文档类型定义)——但它不是一个与上下文无关的语法。
乍一看,这似乎很奇怪;HTML非常接近XML。有很多可用的XML解析器。HTML有一种XML变体——XHTML——这有什么很大的区别呢吗?
区别在于HTML方法更“宽容”:它允许您省略某些标记(然后隐式添加),或者有时省略开始或结束标记,等等。总的来说,它是一种“软”语法,与XML的僵硬和苛刻的语法相反。
这个看起来很小的细节却有很大的不同。一方面,这是HTML如此受欢迎的主要原因:它可以容忍你的错误,让网页作者的生活更轻松。另一方面,它使编写正式语法变得困难。总而言之,传统的解析器无法轻松解析HTML,因为它的语法并不是上下文无关的。HTML不能被XML解析器解析。
HTML DTD
HTML的定义采用DTD格式。此格式用来定义SGML(Standard Generalized Markup Language,标准通用标记语言)家族的语言。该格式包含所有允许的元素、它们的属性和层次结构的定义。正如我们前面看到的,HTML DTD没有形成与上下文无关的语法。
DTD有一些变体。DTD的严格模式完全符合规范;但其他模式包含对过去浏览器使用的标记的支持,目的是向后兼容旧内容。当前的严格DTD在这里:www.w3.org/TR/html4/strict.dtd
DOM
输出树(“解析树”)是DOM元素和属性节点的树。DOM是文档对象模型的简称。它是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。DOM是操作文档的通用规范。其特定模块描述了HTML的特定元素。HTML定义可以在这里找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
当我说树包含DOM节点时,我想表达的是,树是由“实现DOM接口之一”的元素构成的。在浏览器使用的具体实现中,这个元素会包含浏览器内部所需的其他属性。
解析算法
正如我们在前几节中看到的,HTML不能使用常规的自顶向下或自底向上解析器进行解析。
原因如下:
- 该语言本质上非常宽容。
- 2.浏览器能容错的事实。为了支持早期的HTML,浏览器具有对历史版本的容错能力。
- 3.解析的过程中,源可能发生修改。对于其他语言,源在解析过程中不会改变,但在HTML中,动态代码(例如包含
document.write()
调用的脚本元素)可以添加额外的标记,因此解析过程实际上会修改输入。
由于无法使用常规解析技术,浏览器会创建特定的解析器来解析HTML。
解析算法在HTML5规范中有详细描述。该算法分为两个阶段:标记化(tokenization)和树构造(tree construction)。
标记化是词法分析,将输入解析为标记。HTML标记包括开始标记、结束标记、属性名和属性值。
标记器会识别标记,将其提供给树构造函数,并使用下一个字符来识别下一个标记,以此类推,直到输入结束。
标记化算法
算法的输出是一个HTML标记。该算法用一个状态机表示。每个状态消耗输入流的一个或多个字符,并根据这些字符更新下一个状态。该决策受到当前标记化状态和树构造状态的影响。这意味着,根据当前状态的不同,消耗相同的字符将为正确的下一状态产生不同的结果。很难完全描述这个复杂的算法,所以让我们看一个简单的例子来帮助我们理解原理。
基本示例-标记以下HTML:
<html>
<body>
Hello World
</body>
</html>
初始状态是“数据状态”。当遇到<
字符时,状态被更改为“Tag Open状态”。使用一个a-z
字符会导致创建一个“开始标记标记”,状态被更改为“Tag Name状态”。我们一直保持这种状态,直到>
字符被消耗掉。每个字符都被追加到新的标记名。在我们的例子中,创建的标记是一个html
标记。
当到达>
标记时,发出当前标记,状态变回“Data状态”。标记将按相同的步骤处理。到目前为止,html
和body
标签都已发出。现在我们回到了“Data状态”。消耗Hello world
的H
字符将导致创建和释放一个字符标记,直到到达</body>
中的<
为止。我们将为Hello world
的每个字符发出一个字符标记。
我们现在回到了“Tag Open状态”。使用下一个输入/
将导致创建一个结束标记标记,并移动到“Tag Name状态”。我们再次保持这个状态,直到到达>
。然后将发出新的标记标记,我们返回到“Data状态”。输入</html>
将像前一种情况一样处理。
评论
发表评论