二、简介
1、将要讨论的浏览器
2、浏览器的主要功能
3、浏览器的高层结构
4、组件间的通信
三、渲染引擎
1、渲染引擎
2、主流程
3、主流程示例
4、解析和构建DOM树
4.1 解析概述
4.1.1 文法Grammars
4.1.2 解析器——词法分析器
4.1.3 转换
4.1.4 解析实例
词汇表及语法的正式定义
4.1.5 解析器类型
4.1.6 自动化解析
4.2 HTML解析
4.2.1 HTML文法定义
4.2.2 非上下文无法文法
4.2.3 HTML DTD
4.2.4 DOM
4.2.5 解析算法
4.2.6 符号识别算法
4.2.7 树的构建算法
4.2.8 解析结束时的处理
4.2.9 浏览器容错
替代
迷路的表格
嵌套的表单元素
太深的标签继承
放错了地方的HTML、body闭合标签
4.3 CSS解析
4.3.1 WebKit CSS 解析器
4.4 脚本解析
4.5 处理脚本及样式表的顺序
4.5.1 脚本
4.5.2 预解析
4.5.3 样式表
5、构建Render树
5.1 Render树与DOM树的关系
5.2 构建Render树的流程
5.3 样式计算
5.3.1 共享样式数据
5.3.2 Firefox规则树
结构化
使用规则树计算样式上下文
5.3.3 对规则进行处理以简化匹配过程
5.3.4 以正确的级联顺序应用规则
样式表的级联顺序
Specifity
规则排序
5.4 逐步处理
6、布局
6.1 Dirty bit 系统
6.2 全局和增量Layout
6.3 异步和同步Layout
6.4 优化
6.5 Layout过程
6.6 宽度计算
6.7 Line breaking
7、绘制
7.1 全局和增量
7.2 绘制顺序
7.3 Firefox显示列表
7.4 Webkit矩形存储
8、动态变化
9、渲染引擎多线程
9.1 事件循环
10、CSS2可视化模型
10.1 画布 The Canvas
10.2 CSS盒模型
10.3 定位策略 Position scheme
10.4 Box类型
10.5 定位 Position
10.6 Layered representation
参考资源
一、序言
这是一篇全面介绍 Webkit 和 Gecko 内部操作的入门文章,是以色列开发人员塔利·加希尔大量研究的成果。在过去的几年中,她查阅了所有公开发布的关于浏览器内部机制的数据(请参见资源),并花了很多时间来研读网络浏览器的源代码。她写道:
在 IE 占据 90% 市场份额的年代,我们除了把浏览器当成一个
黑箱
,什么也做不了。但是现在,开放源代码的浏览器拥有了过半的市场份额,因此,是时候来揭开神秘的面纱,一探网络浏览器的内幕了。呃,里面只有数以百万行计的 C++ 代码…
塔利在她的网站上公布了自己的研究成果,但是我们觉得它值得让更多的人来了解,所以我们在此重新整理并公布。
作为一名网络开发人员,学习浏览器的内部工作原理将有助于您作出更明智的决策,并理解那些最佳开发实践的个中缘由。尽管这是一篇相当长的文档,但是我们建议您花些时间来仔细阅读;读完之后,您肯定会觉得所费不虚。 —— Paul Irish,Chrome 浏览器开发者
二、简介
网络浏览器很可能是使用最广的软件。在这篇入门文章中,我将会介绍它们的幕后工作原理。我们将看到,从您在地址栏输入 google.com 直到您在浏览器屏幕上看到 Google 首页的整个过程中都发生了些什么。
1、将要讨论的浏览器
现今,主流使用的浏览器主要有:IE、Firefox、Safari、Chrome以及Opera。本文将基于一些开源浏览器的例子进行介绍——Firefox、Chrome以及Safari(部分开源)。
根据W3C(World Wide Web Consortium,万维网联盟)的浏览器统计数据,当前(2011年9月),Firefox、Safari以及Chrome浏览器的市场占有率达到接近60%。因此,可以说开源浏览器占据了浏览器市场的半壁江山。
2、浏览器的主要功能
浏览器的主要功能是将用户选择的Web资源呈现出来,它需要从远端服务器请求资源,并将其显示在浏览器窗口中。这些资源的格式通常是HTML,也包括pdf、image等其他格式。用户通过URI(Uniform Resource Identifier, 统一资源标识符)来指定所请求资源的位置,这部分内容在网络
章节会有更多详细的讨论。
HTML和CSS规范中规定了浏览器解释HTML文档的方式,由W3C组织对这些规范进行维护,W3C是负责指定Web标准的组织。
HTML规范的最新版本是HTML4,HTML5还在制定中(五年前),最新的CSS规范版本是CSS2,CSS3也还在制定中(同样五年前)。
这些年来,浏览器厂商纷纷开发自己的扩展,对W3C规范的遵循并不完善,这为Web开发者带来了严重的兼容性问题。
但是,浏览器的用户界面则差不多,常见的用户界面元素包括:
- 用于输入URI的地址栏;
- 前进、后退按钮;
- 书签选项;
- 用于刷新以及暂停当前加载文档的刷新、暂停按钮;
- 用于到达主页的主页按钮。
奇怪的是,并没有哪个正式公布的规范对用户界面做出规定,这些是多年来各浏览器厂商之间相互模仿和不断改进得结果。
HTML5并没有规定浏览器必须具有的UI元素,但列出了一些常用元素,包括地址栏、状态栏及工具栏。还有一些浏览器有自己专有得功能,比如Firefox得下载管理。更多相关内容将在后面讨论用户界面时介绍。
3、浏览器的高层结构
浏览器的主要组件包括:
- 用户界面 —— 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分;
- 浏览器引擎 —— 用于查询及操作渲染引擎的接口;
- 渲染引擎 —— 用于显示请求的内容,例如,如果请求内容为HTML,渲染引擎负责解析HTML及CSS,并将解析后的结果显示出来;
- 网络 —— 用于完成网络调用,例如HTTP请求,它具有平台无关的接口,可以在不同平台上工作;
- UI后端 —— 用于绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口;
- JS解释器 —— 用于解释执行JS代码;
- 数据存储 —— 属于数据持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了Web Database技术,这是一种轻量级完整的客户端存储技术。
其整体结构图如下所示:
需要注意的是,不同于大部分浏览器,Chrome为每个标签页TAB分配了各自的渲染引擎实例,每个TAB就是一个独立的进程。
对于构成浏览器的这些组件,在后面的内容会逐一进行详细讨论。
4、组件间的通信
Firefox和Chrome都开发了一个特殊的用于通信的基础设施,我们将在后面的专门的一个章节对其进行讨论。
三、渲染引擎
渲染引擎(Rendering Engine)的职责就是渲染,即在浏览器窗口中显示所请求的内容。
默认情况下,渲染引擎可以显示HTML、XML文档及图片,同时,它也可以借助插件(一种浏览器扩展)显示其他类型的数据,例如使用PDF阅读器插件,便可以显示PDF文件。在后面,我们将在专门的一个章节中讲解浏览器插件及扩展。在这里,我们只讨论渲染引擎最主要的用途——显示应用了CSS之后的HTML及图片。
1、渲染引擎
本文所讨论的浏览器——Firefox、Chrome和Safari,它们都是基于两种渲染引擎构建的。其中,Firefox使用Geoko——Mozilla自主研发的渲染引擎,而Safari和Chrome都是使用的WebKit引擎。
WebKit是一款开源的渲染引擎,它本来是为Linux平台研发的,后来由Apple移植到Mac及Windows之上,相关内容可参考WebKit。
2、主流程
渲染引擎首先通过网络获取所请求文档的内容,通常这些文档内容以8k分块的方式完成。
下面是渲染引擎在获取到内容之后的基本处理流程:
- 解析HTML以构建DOM树
- 构建Render树
- 布局Render树
- 绘制Render树
渲染引擎首先会解析HTML,并将标签转化为内容树中的DOM节点。接着,它解析外部CSS以及Style标签中的样式信息,根据这些样式信息,渲染引擎结合HTML中的可见性指令,构建出另一棵树——Render树。
Render树由一些包含颜色、大小等可视化属性的矩形组成,它们将按照正确的顺序显示到屏幕上。
Render树构建完成之后,将会执行布局过程,该过程将确定每个节点在屏幕上的具体坐标。确定完成节点坐标之后,渲染引擎遍历Render树,使用UI后端层绘制每一个节点,这就是最后的绘制Render树过程。
值得注意的是,渲染引擎的工作过程是逐步完成的。为了更好的用户体验,渲染引擎通常会尽可能早地将内容呈现到屏幕上,并不会等到所有的HTML都解析完成之后再去构建和布局Render树。它是解析完成一部分内容就显示一部分内容。在此时,浏览器可能还在通过网络下载页面的其他资源内容。
3、主流程示例
下图是WebKit渲染引擎的主流程示意图:
下图是Gecko渲染引擎的主流程示意图:
结合WebKit主流程图以及Geoko主流程图可以看出,尽管WebKit和Gecko使用的术语稍有不同,它们的主要流程基本相同。Geoko称可见的格式化元素组成的树为Frame树,每个元素都是一个Frame;WebKit则使用Render树来命名由渲染对象组成的树。Webkit中元素的定位称为布局(Layout),而在Geoko中元素的布局称为回流(Reflow)。Webkit中称利用DOM节点及样式信息构建Render树的过程为附着(Attachment),而Geoko则在HTML和DOM树之间附加了一层,并称该层为内容接收层(Content Sink),其相当于制造DOM元素的工厂。
在接下来的内容,我们将详细讨论主流程中的各个阶段。
4、解析和构建DOM树
4.1 解析概述
既然解析是渲染引擎中一个非常重要的过程,在本节,我们将对其进行深入的研究与探讨。
首先,简要介绍一下解析。解析(Parse)一个文档即将其转换为具有一定意义的结构——编码可以理解和使用的东西。解析的结果通常是表达文档结构的节点树,称之为解析树或语法树。
例如,解析2 + 3 - 1
这个表达式,可能返回这样一棵树。
4.1.1 文法Grammars
解析过程需要基于文档所依据的语法规则——文档的语言或格式。每种可被解析的格式必须具有由词汇及语法规则组成的特定的文法,称为上下文无关文法。人类语言不具有这一特性,因此不能被一般的解析技术所解析。
4.1.2 解析器——词法分析器
解析可以分为两个子过程——词法分析与语法分析。
词法分析就是将输入文档分解为符号,符号是语言的词汇表——基本有效单元的集合。对于人类语言来说,它相当于我们字典中出现的所有单词。
语法分析值得是对语言应用语法规则。
解析器一般讲工作分配给两个组件——词法分析器(Lexer,有时也称之为分词器)负责将输入文档分解为合法的符号,解析器(Parser)则根据语言的语法规则分析文档结构,从而构建解析树。词法分析器知道怎么跳过空格和换行之类的无关字符。
解析过程是迭代的,解析器从词法分析器取到一个新的符号,并试着用这个符号匹配一条语法规则。如果匹配到了一条规则,这个符号对应的节点将被添加到解析树上,然后解析树请求另一个符号。如果没有匹配到规则,解析器将在内部保存该符号,并从词法分析器去下一个符号,指导所有内部保存的符号能够匹配到一条语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着输入文档无效或是包含语法错误。
4.1.3 转换
很多时候,解析树并不是最终结果。解析一般在转换中使用,即将输入文档转换为另一种格式。编译器中的编译就是这样一个例子,编译器在将一段源码编译为机器码时,首先将源码解析为解析树,然后再将该解析树转换为机器码文档。
4.1.4 解析实例
在前面,我们从一个数学表达式构建了一棵解析树,这里定义一个简单的数学语言来展现一下解析过程。
词汇表:包括整数、加号及减号。
语法:
- 该语言的语法基本单元包括表达式、term及操作符;
- 该语言可以包括多个表达式;
- 一个表达式定义为两个term,通过一个操作符连接;
- 操作符可以是加号或者减号;
- term可以是一个整数或一个表达式。
现在,我们来分析一下2 + 3 - 1
这个输入。
第一个匹配规则的子字符串是2
,根据规则5,它是一个term;第二个匹配的是2 + 3
,它符合规则3 —— 一个操作符连接两个term,下一次匹配发生在输入的结束处;2 + 3 - 1
是一个表达式,因为我们已经知道2 + 3
是一个term,所以我们有了一个term紧跟着一个操作符及另一个term。
对于表达式2 + +
,其不会匹配任何规则,因此是一个无效的输入。
词汇表及语法的正式定义
词汇表通常利用正则表达式来定义。
例如,上面的语言可以定义为:
INTEGER: 0 | [1-9] [0-9] *
PLUS: +
MINUS: -
正如所看到的,这里用正则表达式定义整数。
语法通常用BNF格式定义,我们的语言可以定义为:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
如果一个语言的文法是上下文无关的,则它可以用正则解析器来解析。对上下文无关文法的一个直观定义就是,该文法可以用BNF来完整表达。可查看Context-Free Grammar。
4.1.5 解析器类型
有两种基本的解析器——自顶向下解析以及自底向上解析。比较直观的解释就是,自顶向下解析,查看语法的最高层结构并试着匹配其中一个;自底向上解析则从输入开始,逐步将其转换为语法规则,从底层规则开始直到匹配最高规则。
来看一下这两种解析器如何解析上面的2 + 3 - 1
例子:
自顶向下解析器从最高层规则开始——它先识别出2+3
,将其视为一个表达式,然后识别出2+3-1
为一个表达式(识别表达式的过程中匹配了其他规则,但出发点是最高层规则)。
自底向上解析会扫描输入直到匹配了一条规则,然后用该规则取代匹配的输入,直到解析完所有输入。部分匹配的表达式被放置在解析堆栈中。
Stack | Input |
---|---|
2+3-1 | |
term | +3-1 |
term operation | 3-1 |
expression | -1 |
expression operation | 1 |
expression |
自底向上解析器称为shift reduce解析器,因为输入向右移动(想象一个指针首先指向输入开始处,并向右移动),并逐渐简化为语法规则。
4.1.6 自动化解析
解析器生成器这个工具可以自动生成解析器,只需要指定语言的文法——词汇表及语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动地创建一个有较好性能的解析器并不容易,因此解析器生成器十分有用。
WebKit使用两个知名的解析器生成器——用于创建语法分析器的Flex以及用于创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的语法规则。
4.2 HTML解析
HTML解析器的工作就是将HTML文档解析为解析树。
4.2.1 HTML文法定义
W3C组织制定的HTML规范定义了HTML的词汇表和语法规则。
4.2.2 非上下文无法文法
正如在解析一节提到的,上下文无关文法的语法可以用类似BNF的格式来定义。
不幸的是,所有的传统解析方式都不适用于HTML(当然我提出它们并不只是因为好玩,它们将用来解析CSS和JavaScript),HTML不能简单地用解析所需的上下文无关文法来定义。
HTML有一个正式的格式定义——DTD(Document Type Definition,文档类型定义),但它并不是上下文无关文法。HTML更接近于XML,现在有很多可用的XML解析器,HTML有个XML的变体,即XHTML。它们之间的不同在于,HTML更加宽容,它允许忽略一些特定的标签,有时可以省略开始或结束标签。总的来说,它是一种软语法,不像XML一般呆板。
显然,这个看起来很小的差异却带来了很大的不同。一方面,这是HTML流行的原因——它的包容性使Web开发人员的工作更加轻松;但另一方面,这也使得很难去写一个格式化的文法。所以,HTML的解析并不简单,它既不能用传统的解析器解析,也不能用XML解析器解析。
4.2.3 HTML DTD
HTML适用DTD格式进行定义,这一格式适用于定义SGML家族的语言,包括了对所有允许元素及它们的属性和层次关系的定义。正如前面提到的,HTML DTD并没有生成一种上下文无关文法。
DTD有一些变种,标准模式只遵守规范,而其他模式则包含了对浏览器过去所使用的标签的支持,这样做是为了兼容以前的内容。最新的标准DTD在DTD。
4.2.4 DOM
解析输出的树,也就是解析树,是由DOM元素及属性节点组成的。DOM是文档对象模型(Document Object Model)的缩写。它是HTML文档的对象表示,作为HTML元素的外部接口供JS等调用。
DOM树的根是Document
对象。
DOM和标签基本是一一对应的关系。例如,如下的标签:
Hello DOM
- src=`example.png`/>
将会被转换为下面的DOM树:
与HTML一样,DOM的规范也是由W3C组织制定的。访问http://www.w3.org/DOM/DOMTR,这是使用文档的一般规范。一个模型描述一种特定的HTML元素,可以在http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.htm查看HTML定义。
当我们说DOM树包含许多DOM节点,指的是这棵DOM树是由许多实现了DOM接口的元素构建而成的。浏览器使用已被浏览器内部使用的其他属性的具体实现。
4.2.5 解析算法
正如前面章节中所讨论的,HTML不能被一般的自顶向下或自底向上的解析器所解析。原因是:
- 这门语言本身的宽容特性;
- 浏览器对一些常见的非法HTML有容错机制;
- 解析过程是往复的,通常源码不会在解析过程中发生改变。但在HTML中,脚本标签包含的
document.write
可能添加标签,这说明在解析过程中实际修改了输入的文档内容。
对于HTML解析,不能使用正则解析技术,浏览器为HTML定制了专门的解析器。
HTML5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。
符号化(Tokenization)是词法分析的过程,将输入解析为符号,HTML的符号包括开始标签、结束标签、属性名以及属性值。符号识别器识别出符号后,将其传递给DOM树构建器(Tree constructor),并读取下一个字符,再进行符号识别,这样直到处理完所有输入。
4.2.6 符号识别算法
算法输出HTML符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。
这个算法很复杂,这里用一个简单的例子来解释这个原理。
基本示例——符号化下面的HTML代码:
Hello world
初始状态为Data State
,当遇到<
字符,状态变为Tag open state
,读取一个a-z的字符将产生一个开始标签符号,状态相应变为Tag name state
,一直保持这个状态直到读取到>
,每个字符都附加到这个符号名上,例子中创建的是一个HTML符号。
当读取到>
,当前的符号就完成了,此时,状态回到Data state
,重复这一处理过程。到这里,HTML和body标签都识别出来了。现在,回到
Data state
,读取Hello world
中的字符H
将创建并识别出一个字符符号,这里会为Hello world
中的每个字符生成一个字符符号。
这样直到遇到中的
<
。现在,又回到了Tag open state
,读取下一个字符/
将创建一个闭合标签符号,并且状态转移到Tag name state
,还是保持这一状态,直到遇到>
。然后,产生一个新的标签符号并回到Data state
。后面的