现代浏览器的工作原理

一、序言
二、简介
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 解析实例

在前面,我们从一个数学表达式构建了一棵解析树,这里定义一个简单的数学语言来展现一下解析过程。

现代浏览器的工作原理

词汇表:包括整数、加号及减号。

语法

  1. 该语言的语法基本单元包括表达式、term及操作符;
  2. 该语言可以包括多个表达式;
  3. 一个表达式定义为两个term,通过一个操作符连接;
  4. 操作符可以是加号或者减号;
  5. term可以是一个整数或一个表达式。

现在,我们来分析一下2 + 3 - 1这个输入。

第一个匹配规则的子字符串是2,根据规则5,它是一个term;第二个匹配的是2 + 3,它符合规则3 —— 一个操作符连接两个term,下一次匹配发生在输入的结束处;2 + 3 - 1是一个表达式,因为我们已经知道2 + 3是一个term,所以我们有了一个term紧跟着一个操作符及另一个term。

对于表达式2 + +,其不会匹配任何规则,因此是一个无效的输入。

词汇表及语法的正式定义

词汇表通常利用正则表达式来定义。

例如,上面的语言可以定义为:

  1. INTEGER: 0 | [1-9] [0-9] *
  2. PLUS: +
  3. MINUS: -

正如所看到的,这里用正则表达式定义整数。

语法通常用BNF格式定义,我们的语言可以定义为:

  1. expression := term operation term
  2. operation := PLUS | MINUS
  3. 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和标签基本是一一对应的关系。例如,如下的标签:

  1. <HTML>
  2. <body>
  3. <p>
  4. Hello DOM
  5. </p>
  6. <div><img src=`example.png`/></div>
  7. </body>
  8. </HTML>

将会被转换为下面的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代码:

  1. <HTML>
  2. <body>
  3. Hello world
  4. </body>
  5. </HTML>

初始状态为Data State,当遇到<字符,状态变为Tag open state,读取一个a-z的字符将产生一个开始标签符号,状态相应变为Tag name state,一直保持这个状态直到读取到>,每个字符都附加到这个符号名上,例子中创建的是一个HTML符号。

当读取到>,当前的符号就完成了,此时,状态回到Data state<body>重复这一处理过程。到这里,HTML和body标签都识别出来了。现在,回到Data state,读取Hello world中的字符H将创建并识别出一个字符符号,这里会为Hello world中的每个字符生成一个字符符号。

这样直到遇到</body>中的<。现在,又回到了Tag open state,读取下一个字符/将创建一个闭合标签符号,并且状态转移到Tag name state,还是保持这一状态,直到遇到>。然后,产生一个新的标签符号并回到Data state。后面的</HTML>将和</body>一样处理。

具体流程如下图:
现代浏览器的工作原理

4.2.7 树的构建算法

在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的DOM元素,对应的DOM元素将会被创建。这些元素除了会被添加到DOM树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。

来看一下示例中树的构建过程:

  1. <HTML>
  2. <body>
  3. Hello world
  4. </body>
  5. </HTML>

构建树这一阶段的输入是符号识别阶段生成的符号序列。

首先是initial mode,接收到HTML符号后将转换为before HTML模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHTMLElement元素,并将其附加到根Document对象上。

状态此时变为before head,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。

现在,转到in head模式,然后是after head。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到in body模式。

然后,接收到字符串Hello world的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。

接收到body结束符号时,转移到after body模式,接着接收到HTML结束符号,这个符号意味着转移到了after after body模式,当接收到文件结束符时,整个解析过程结束。

具体流程如下图:

现代浏览器的工作原理

4.2.8 解析结束时的处理

在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。

文档状态将被设置为完成,同时触发一个LOAD事件。

HTML5规范中有符号化及构建树的完整算法Parsing HTML documents

4.2.9 浏览器容错

你从来不会在HTML页面看到无效语法这种错误,浏览器修复了无效内容并继续工作。

以下面这段HTML代码为例:

  1. <HTML>
  2. <mytag>
  3. </mytag>
  4. <div>
  5. <p>
  6. </div>
  7. Really lousy HTML
  8. </p>
  9. </HTML>

这段HTML代码违反了很多规则(mytag不是合法的标签,p及div错误的嵌套等等),但是浏览器仍然可以没有任何怨言的继续显示,它在解析的过程中修复了HTML代码作者的错误。

浏览器都具有错误处理的能力,但是,另人惊讶的是,这并不是HTML最新规范的内容,就像书签及前进后退按钮一样,它只是浏览器长期发展的结果。一些比较知名的非法HTML结构,在许多站点中出现过,浏览器都试着以一种和其他浏览器一致的方式去修复。

HTML5规范定义了这方面的需求,WebKit在HTML解析类开始部分的注释中做了很好的总结。

解析器将符号化的输入解析为文档并创建文档,但不幸的是,我们必须处理很多没有很好格式化的HTML文档,至少要小心下面几种错误情况:

  1. 在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合。

  2. 不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTML、HEAD、BODY、TR、TD、LI等。

  3. 想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素。

  4. 如果这些都不行,就闭合当前标签直到可以添加该元素。

下面,我们来看一些WebKit容错的例子:

</br> 替代 <br>

一些网站使用</br>替代<br>,为了兼容IE和Firefox,WebKit将其统一看做<br>

代码:

  1. if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
  2. reportError(MalformedBRError);
  3. t->beginTag = true;
  4. }

注意:这里的错误处理在内部进行,用户看不到。

迷路的表格

这里的迷路的表格指的是一个表格嵌套在另一个表格中,但不在它的某个单元格内。

比如下面这个例子:

  1. <table>
  2. <table>
  3. <tr><td>inner table</td></tr>
  4. </table>
  5. <tr><td>outer table</td></tr>
  6. </table>

WebKit会将嵌套的表格变为两个兄弟表格:

  1. <table>
  2. <tr><td>outer table</td></tr>
  3. </table>
  4. <table>
  5. <tr><td>inner table</td></tr>
  6. </table>

代码:

  1. if (m_inStrayTableContent && localName == tableTag)
  2. popBlock(tableTag);

WebKit使用堆栈存放当前的元素内容,它将外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。

嵌套的表单元素

用户将一个表单嵌套到另一个表单中,则第二个表单将被忽略。

代码:

  1. if (!m_currentFormElement) {
  2. m_currentFormElement = new HTMLFormElement(formTag, m_document);
  3. }
太深的标签继承

代码:

  1. // www.liceo.edu.mx是一个由嵌套层次的站点的例子,最多只允许20个相同类型的标签嵌套,多出来的将被忽略。
  2. bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
  3. {
  4. unsigned i = 0;
  5. for (HTMLStackElem* curr = m_blockStack;
  6. i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
  7. curr = curr->next, i++) {
  8. }
  9. return i != cMaxRedundantTagDepth;
  10. }
放错了地方的HTML、body闭合标签

又一次不言自明。

代码:

  1. // 支持不完整的HTML。我们从来不闭合body,因为一些愚蠢的网页总是在还未真正结束时就闭合它。我们依赖调用end方法去执行关闭的处理。
  2. if (t->tagName == HTMLTag || t->tagName == bodyTag )
  3. return;

所以,Web开发者要小心了,除非你想成为WebKit容错代码的范例,否则还是写格式良好的HTML吧。

4.3 CSS解析

还记得解析概述中提到的解析的概念吗。不同与HTML,CSS属于上下文无关文法,可以用前面所描述的解析器来解析。CSS规范定义了CSS的词法及语法文法。

看一些例子:

每个符号都是由正则表达式来定义词法文法(词汇表)的:

  1. comment ///*[^*]*/*+([^/*][^*]*/*+)*//
  2. num [0-9]+|[0-9]*`.`[0-9]+
  3. nonascii [/200-/377]
  4. nmstart [_a-z]|{nonascii}|{escape}
  5. nmchar [_a-z0-9-]|{nonascii}|{escape}
  6. name {nmchar}+
  7. ident {nmstart}{nmchar}*

ident是识别器的缩写,相当于一个class名,name是一个元素id(用引用)。

语法用BNF进行描述:

  1. ruleset
  2. : selector [ ',' S* selector ]*
  3. ‘{’ S* declaration [ ';' S* declaration ]* ‘}’ S*
  4. ;
  5. selector
  6. : simple_selector [ combinator selector | S+ [ combinator selector ] ]
  7. ;
  8. simple_selector
  9. : element_name [ HASH | class | attrib | pseudo ]*
  10. | [ HASH | class | attrib | pseudo ]+
  11. ;
  12. class
  13. : ‘.’ IDENT
  14. ;
  15. element_name
  16. : IDENT | ‘*’
  17. ;
  18. attrib
  19. : ‘[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
  20. [ IDENT | STRING ] S* ] ‘]’
  21. ;
  22. pseudo
  23. : ‘:’ [ IDENT | FUNCTION S* [IDENT S*] ‘)’ ]
  24. ;

说明:一个规则集合有这样的结构:

  1. div.error , a.error {
  2. color:red;
  3. font-weight:bold;
  4. }

div.errora.error是选择器,大括号中的内容包含了这条规则集合中的规则。这个结构正式定义在下面的定义中:

  1. ruleset
  2. : selector [ ',' S* selector ]*
  3. ‘{’ S* declaration [ ';' S* declaration ]* ‘}’ S*
  4. ;

这说明,一个规则集合具有一个或是可选个数的多个选择器,这些选择器以逗号和空格(S表示空格)进行分隔。每个规则集合包含大括号及大括号中的一条或多条以分号隔开的声明。声明和选择器在后面进行定义。

4.3.1 WebKit CSS 解析器

Webkit使用Flex和Bison解析生成器从CSS语法文件中自动生成解析器。回忆一下解析器的介绍,Bison创建一个自底向上的解析器,Firefox使用自顶向下解析器。它们都是将每个CSS文件解析为样式表对象,每个对象包含CSS规则,CSS规则对象包含选择器和声明对象,以及其他一些符合CSS语法的对象。

现代浏览器的工作原理

4.4 脚本解析

本部分将介绍Javascript。此处留白,设置占位符。

4.5 处理脚本及样式表的顺序

4.5.1 脚本

Web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完。如果脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在HTML4及HTML5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。HTML5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。

4.5.2 预解析

Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。

需要注意的是,预解析并不改变DOM树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

4.5.3 样式表

样式表采用另一种不同的模式。理论上,既然样式表不改变DOM树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。

5、构建Render树

当DOM树构建完成时,浏览器开始构建另一棵树——渲染树(Render Tree)。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。

Firefox将渲染树中的元素称为frames,WebKit则用renderer或渲染对象来描述这些元素。

一个渲染对象知道怎么布局及绘制自己及它的children。

RenderObject是WebKit的渲染对象基类,它的定义如下:

  1. class RenderObject{
  2. virtual void layout();
  3. virtual void paint(PaintInfo);
  4. virtual void rect repaintRect();
  5. Node* node; //the DOM node
  6. RenderStyle* style; // the computed style
  7. RenderLayer* containgLayer; //the containing z-index layer
  8. }

每个渲染对象用一个和该节点的CSS盒模型相对应的矩形区域来表示。正如CSS2所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响。下面的WebKit代码说明了如何根据display属性决定某个节点创建何种类型的渲染对象。

  1. RenderObject* RenderObject::createObject(Node* node, RenderStyle* style) {
  2. Document* doc = node->document();
  3. RenderArena* arena = doc->renderArena();
  4. RenderObject* o = 0;
  5. switch (style->display()) {
  6. case NONE:
  7. break;
  8. case INLINE:
  9. o = new (arena) RenderInline(node);
  10. break;
  11. case BLOCK:
  12. o = new (arena) RenderBlock(node);
  13. break;
  14. case INLINE_BLOCK:
  15. o = new (arena) RenderBlock(node);
  16. break;
  17. case LIST_ITEM:
  18. o = new (arena) RenderListItem(node);
  19. break;
  20. }
  21. return o;
  22. }

元素的类型也需要考虑,例如,表单控件和表格带有特殊的框架。

在WebKit中,如果一个元素想创建一个特殊的渲染对象,它需要复写createRenderer方法,使渲染对象指向不包含几何信息的样式对象。

5.1 Render树与DOM树的关系

渲染对象和DOM元素相对应,但这种对应关系不是一对一的,不可见的DOM元素不会被插入渲染树,例如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出现在渲染树中)。

还有一些DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而折行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的HTML,根据CSS规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会创建匿名的块状渲染对象包裹住行内元素。

一些渲染对象和所对应的DOM节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。

现代浏览器的工作原理

5.2 构建Render树的流程

Firefox中,构建Render树可以表述为一个监听Dom更新的监听器,将frame的创建委派给Frame Constructor,这个构建器计算样式(参看样式计算)并创建一个frame。

WebKit中,计算样式并生成渲染对象的过程称为attachment,每个DOM节点有一个attach方法,attachment的过程是同步的,调用新节点的attach方法将节点插入到DOM树中。

处理HTML和body标签将构建渲染树的根,这个根渲染对象对应被CSS规范称为containing block的元素——包含了其他所有块元素的顶级块元素。它的大小就是ViewPort——浏览器窗口的显示区域,Firefox称它为ViewPortFrame,WebKit称为RenderView,这个就是文档所指向的渲染对象,树中其他的部分都将作为一个插入的DOM节点被创建。

5.3 样式计算

创建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。

样式包括各种来源的样式表,行内样式元素及HTML中的可视化属性(例如bgcolor),可视化属性转化为CSS样式属性。

样式表来源于浏览器默认样式表,及页面作者和用户提供的样式表——有些样式是浏览器用户提供的(浏览器允许用户定义喜欢的样式,例如,在Firefox中,可以通过在Firefox Profile目录下放置样式表实现)。

计算样式的一些困难:

(1)样式数据是非常大的结构,保存大量的样式属性会带来内存问题。

(2)如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。

例如,下面这个复杂选择符

  1. div div div div{…}

这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试。

(3)应用规则涉及非常复杂的级联,它们定义了规则的层次.

下面,我们来看一下浏览器如何处理这些问题:

5.3.1 共享样式数据

WebKit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:

  • 这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)
  • 不能有元素具有id
  • 标签名必须匹配
  • class属性必须匹配
  • 对应的属性必须相同
  • 链接状态必须匹配
  • 焦点状态必须匹配
  • 不能有元素被属性选择器影响
  • 元素不能有行内样式属性
  • 不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。

5.3.2 Firefox规则树

Firefox使用两个树来简化样式计算——规则树和样式上下文树。WebKit也有样式对象,但它们并没有存储在类似样式上下文这样的树中,只是由DOM节点指向其相关的样式。

现代浏览器的工作原理
样式上下文包含最终值,这些值是通过以正确顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值,例如,如果逻辑值为屏幕的百分比,则通过计算将其转化为绝对单位。样式树的使用确实很巧妙,它使得在节点中共享的这些值不需要被多次计算,同时也节省了存储空间。

所有匹配的规则都存储在规则树中,一条路径中的底层节点拥有最高的优先级,这棵树包含了所找到的 所有规则匹配的路径(译注:可以取巧理解为每条路径对应一个节点,路径上包含了该节点所匹配的所有规则)。规则树并不是一开始就为所有节点进行计算,而是 在某个节点需要计算样式时,才进行相应的计算并将计算后的路径添加到树中。

我们将树上的路径看成辞典中的单词,假如已经计算出了如下的规则树:

现代浏览器的工作原理

假如需要为内容树中的另一个节点匹配规则,现在知道匹配的规则(以正确的顺序)为B-E-I,因为我们已经计算出了路径A-B-E-I-L,所以树上已经存在了这条路径,剩下的工作就很少了。

现在来看一下树如何保存。

结构化

样式上下文按结构划分,这些结构包括类似border或color这样的特定分类的样式信息。一个结构中的所有特性不是继承的就是非继承的,对继承的特性,除非元素自身有定义,否则就从它的parent继承。非继承的特性(称为reset特性)如果没有定义,则使用默认的值。

样式上下文树缓存完整的结构(包括计算后的值),这样,如果底层节点没有为一个结构提供定义,则使用上层节点缓存的结构。

使用规则树计算样式上下文

当为一个特定的元素计算样式时,首先计算出规则树中的一条路径,或是使用已经存在的一条,然后使用路径中的规则去填充新的样式上下文,从样式的底层节点开始,它具有最高优先级(通常是最特定的选择器),遍历规则树,直到填满结构。如果在那个规则节点 没有定义所需的结构规则,则沿着路径向上,直到找到该结构规则。

如果最终没有找到该结构的任何规则定义,那么如果这个结构是继承型的,则找到其在内容树中的parent的结构,这种情况下,我们也成功的共享了结构;如果这个结构是reset型的,则使用默认的值。

如果特定的节点添加了值,那么需要做一些额外的计算以将其转换为实际值,然后在树上的节点缓存该值,使它的children可以使用。

当一个元素和它的一个兄弟元素指向同一个树节点时,完整的样式上下文可以被它们共享。

来看一个例子:假设有下面这段HTML代码。

  1. <HTML>
  2. <body>
  3. <div class=`err` id=`div1″>
  4. <p>this is a
  5. <span class=`big`> big error </span>
  6. this is also a
  7. <span class=`big`> very big error</span>
  8. error
  9. </p>
  10. </div>
  11. <div class=`err` id=`div2″>another error</div>
  12. </body>
  13. </HTML>

以及下面这些规则:

  1. 1. div {margin:5px;color:black}
  2. 2. .err {color:red}
  3. 3. .big {margin-top:3px}
  4. 4. div span {margin-bottom:4px}
  5. 5. #div1 {color:blue}
  6. 6. #div2 {color:green}

简化下问题,我们只填充两个结构——color和margin,color结构只包含一个成员-颜色,margin结构包含四边。

生成的规则树如下(节点名:指向的规则):

现代浏览器的工作原理

上下文树如下:

现代浏览器的工作原理

假设我们解析HTML,遇到第二个div标签,我们需要为这个节点创建样式上下文,并填充它的样式结构。

我们进行规则匹配,找到这个div匹配的规则为1、2、6,我们发现规则树上已经存在了一条我们可以使用的路径1、2,我们只需为规则6新增一个节点添加到下面(就是规则树中的F)。

然后创建一个样式上下文并将其放到上下文树中,新的样式上下文将指向规则树中的节点F。

现在我们需要填充这个样式上下文,先从填充margin结构开始,既然最后一个规则节点没有添加margin结构,沿着路径向上,直到找到缓存的前面插入节点计算出的结构,我们发现B是最近的指定margin值的节点。因为已经有了color结构的定义,所以不能使用缓存的结构,既然color只有一个属性,也就不需要沿着路径向上填充其他属性。计算出最终值(将字符串转换为RGB等),并缓存计算后的结构。

第二个span元素更简单,进行规则匹配后发现它指向规则G,和前一个span一样,既然有兄弟节点指向同一个节点,就可以共享完整的样式上下文,只需指向前一个span的上下文。

因为结构中包含继承自parent的规则,上下文树做了缓存(color特性是继承来的,但Firefox将其视为reset并在规则树中缓存)。

例如,如果我们为一个paragraph的文字添加规则:

p {font-family:Verdana;font size:10px;font-weight:bold}

那么这个p在内容树中的子节点div,会共享和它parent一样的font结构,这种情况发生在没有为这个div指定font规则时。

Webkit中,并没有规则树,匹配的声明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这些属性,是因为其他的依赖于它们-比如display),其次是高优先级important的,接着是一般优先级非important的,最后是一般优先级important的规则。这样,出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。

总结一下,共享样式对象(结构中完整或部分内容)解决了问题1和3,Firefox的规则树帮助以正确的顺序应用规则。

5.3.3 对规则进行处理以简化匹配过程

样式规则有几个来源:

  • 外部样式表或style标签内的CSS规则;

  • 行内样式属性;

  • HTML可视化属性(映射为相应的样式规则)。

后面两个很容易匹配到元素,因为它们所拥有的样式属性和HTML属性可以将元素作为key进行映射。

就像前面问题2所提到的,CSS的规则匹配可能很狡猾,为了解决这个问题,可以先对规则进行处理,以使其更容易被访问。

解析完样式表之后,规则会根据选择符添加一些hash映射,映射可以是根据id、class、标签名或是任何不属于这些分类的综合映射。如果选择符为id,规则将被添加到id映射,如果是class,则被添加到class映射,等等。

这个处理是匹配规则更容易,不需要查看每个声明,我们能从映射中找到一个元素的相关规则,这个优化使在进行规则匹配时减少了95+%的工作量。

来看下面的样式规则:

  1. p.error {color:red}
  2. #messageDiv {height:50px}
  3. div {margin:5px}

第一条规则将被插入class映射,第二条插入id映射,第三条是标签映射。

下面这个HTML片段:

  1. <p class=`error`>an error occurred </p>
  2. <div id=` messageDiv`>this is a message</div>

我们首先找到p元素对应的规则,class映射将包含一个error的key,找到p.error的规则,div在id映射和标签映射中都有相关的规则,剩下的工作就是找出这些由key对应的规则中哪些确实是正确匹配的。

例如,如果div的规则是

  1. table div {margin:5px}

这也是标签映射产生的,因为key是最右边的选择符,但它并不匹配这里的div元素,因为这里的div没有table祖先。

WebKit和Firefox都会做这个处理。

5.3.4 以正确的级联顺序应用规则

样式对象拥有对应所有可见属性的属性,如果特性没有被任何匹配的规则所定义,那么一些特性可以从parent的样式对象中继承,另外一些使用默认值。

这个问题的产生是因为存在不止一处的定义,这里用级联顺序解决这个问题。

样式表的级联顺序

一个样式属性的声明可能在几个样式表中出现,或是在一个样式表中出现多次,因此,应用规则的顺序至关重要,这个顺序就是级联顺序。根据CSS2的规范,级联顺序为(从低到高):

  1. 浏览器声明
  2. 用户声明
  3. 作者的一般声明
  4. 作者的important声明
  5. 用户important声明

浏览器声明是最不重要的,用户只有在声明被标记为important时才会覆盖作者的声明。具有同等级别的声明将根据specifity以及它们被定义时的顺序进行排序。HTML可视化属性将被转换为匹配的CSS声明,它们被视为最低优先级的作者规则。

Specifity

CSS2规范中定义的选择符specifity如下:

  • 如果声明来自style属性,而不是一个选择器的规则,则计1,否则计0(=a)
  • 计算选择器中id属性的数量(=b)
  • 计算选择器中class及伪类的数量(=c)
  • 计算选择器中元素名及伪元素的数量(=d)

连接a-b-c-d四个数量(用一个大基数的计算系统)将得到specifity。这里使用的基数由分类中最高的基数定义。例如,如果a为14,可以使用16进制。不同情况下,a为17时,则需要使用阿拉伯数字17作为基数,这种情况可能在这个选择符时发生html body div div …(选择符中有17个标签,一般不太可能)。

一些例子:

  1. * {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
  2. li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
  3. li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
  4. ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
  5. ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
  6. h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
  7. ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
  8. li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
  9. #x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
  10. style=`` /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
规则排序

规则匹配后,需要根据级联顺序对规则进行排序,webkit先将小列表用冒泡排序,再将它们合并为一个大列表,webkit通过为规则复写>操作来执行排序:

  1. static bool operator >(CSSRuleData& r1, CSSRuleData& r2) {
  2. int spec1 = r1.selector()->specificity();
  3. int spec2 = r2.selector()->specificity();
  4. return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
  5. }

5.4 逐步处理

WebKit使用一个标志位标识所有顶层样式表都已加载,如果在attch时样式没有完全加载,则放置占位符,并在文档中标记,一旦样式表完成加载就重新进行计算。

6、布局

当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程称为layout或reflow,即为布局。

HTML使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左、自上而下的进行。也存在一些例外,比如html tables。

坐标系统相对于根frame,使用top和left坐标。

布局是一个递归的过程,由根渲染对象开始,它对应HTML文档元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。

根渲染对象的位置是0,0,它的大小是viewport-浏览器窗口的可见部分。

所有的渲染对象都有一个layout或reflow方法,每个渲染对象调用需要布局的children的layout方法。

6.1 Dirty bit 系统

为了不因为每个小变化都全部重新布局,浏览器使用一个dirty bit系统,一个渲染对象发生了变化或是被添加了,就标记它及它的children为dirty,即需要进行重新Layout。存在两个标识-dirty及children are dirty,children are dirty说明即使这个渲染对象可能没问题,但它至少有一个child需要layout。

6.2 全局和增量Layout

当Layout在整棵渲染树触发时,称为全局Layout,这可能在下面这些情况下发生:

  1. 一个全局的样式改变影响所有的渲染对象,比如字号的改变;

  2. 窗口resize。

Layout也可以是增量的,这样只有标志为dirty的渲染对象会重新布局(也将导致一些额外的布局)。增量Layout会在渲染对象dirty时异步触发,例如,当网络接收到新的内容并添加到DOM树后,新的渲染对象会添加到渲染树中。

现代浏览器的工作原理

6.3 异步和同步Layout

增量Layout的过程是异步的,Firefox为增量Layout生成了reflow队列,以及一个调度器以执行这些批处理命令。WebKit也有一个计时器用来执行增量Layout —— 遍历树,为dirty状态的渲染对象重新布局。

另外,当脚本请求样式信息时,例如offsetHeight,会同步的触发增量布局。

全局的Layout一般都是同步触发。有些时候,Layout会被作为一个初始Layout之后的回调,比如滑动条的滑动。

6.4 优化

当一个Layout因为resize或是渲染位置改变(并不是大小改变)而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。

一般情况下,如果只有子树发生改变,则Layout并不从根开始。这种情况发生在,变化发生在元素自身并且不影响它周围元素,例如,将文本插入文本域(否则,每次击键都将触发从根开始的重排)。

6.5 Layout过程

Layout一般有下面这几个部分:

(1) parent渲染对象决定它的宽度;

(2)parent渲染对象读取chilidren,并:

  • 放置child渲染对象(设置它的x和y);

  • 在需要时(它们当前为dirty或是处于全局layout或者其他原因)调用child渲染对象的layout,这将计算child的高度;

(3)parent渲染对象使用child渲染对象的累积高度,以及margin和padding的高度来设置自己的高度-这将被parent渲染对象的parent使用;

(4)将dirty标识设置为false。

Firefox使用一个state对象(nsHTMLReflowState)做为参数去布局(firefox称为reflow),state包含parent的宽度及其他内容。

Firefox布局的输出是一个metrics对象(nsHTMLReflowMetrics)。它包括渲染对象计算出的高度。

6.6 宽度计算

渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度及margin、border进行计算。例如,下面这个div的宽度:

  1. <div style=`width:30%`/>

WebKit中宽度的计算过程是(RenderBox类的calcWidth方法):

  • 容器的宽度是容器的可用宽度和0中的最大值,这里的可用宽度为:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight代表一个对象内部的不包括border和滑动条的大小;

  • 元素的宽度指样式属性width的值,它可以通过计算容器的百分比得到一个绝对值;

  • 加上水平方向上的border和padding。

到这里是最佳宽度的计算过程,现在计算宽度的最大值和最小值,如果最佳宽度大于最大宽度则使用最大宽度,如果小于最小宽度则使用最小宽度。最后缓存这个值,当需要layout但宽度未改变时使用。

6.7 Line breaking

当一个渲染对象在布局过程中需要折行时,则暂停并告诉它的parent它需要折行,parent将创建额外的渲染对象并调用它们的layout。

7、绘制

绘制阶段,遍历渲染树并调用渲染对象的paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件,这在UI的章节有更多的介绍。

7.1 全局和增量

和布局一样,绘制也可以是全局的,绘制完整的树,或增量的。在增量的绘制过程中,一些渲染对象以不影响整棵树的方式改变,改变的渲染对象使其在屏幕上的矩形区域失效,这将导致操作系统将其看作dirty区域,并产生一个paint事件,操作系统很巧妙的处理这个过程,并将多个区域合并为一个。Chrome中,这个过程更复杂些,因为渲染对象在不同的进程中,而不是在主进程中。Chrome在一定程度上模拟操作系统的行为,表现为监听事件并派发消息给渲染根,在树中查找到相关的渲染对象,重绘这个对象(往往还包括它的children)。

7.2 绘制顺序

css2定义了绘制过程的顺序-http://www.w3.org/TR/CSS21/zindex.html。这个就是元素压入堆栈的顺序,这个顺序影响着绘制,堆栈从后向前进行绘制。

一个块渲染对象的堆栈顺序是:

  1. 背景色
  2. 背景图
  3. border
  4. children
  5. outline

7.3 Firefox显示列表

Firefox读取渲染树并为绘制的矩形创建一个显示列表,该列表以正确的绘制顺序包含这个矩形相关的渲染对象。

用这样的方法,可以使重绘时只需查找一次树,而不需要多次查找——绘制所有的背景、所有的图片、所有的border等等。

Firefox优化了这个过程,它不添加会被隐藏的元素,比如元素完全在其他不透明元素下面。

7.4 Webkit矩形存储

重绘前,webkit将旧的矩形保存为位图,然后只绘制新旧矩形的差集。

8、动态变化

浏览器总是试着以最小的动作响应一个变化,所以一个元素颜色的变化将只导致该元素的重绘,元素位置的变化将大致元素的布局和重绘,添加一个DOM节点,也会大致这个元素的布局和重绘。一些主要的变化,比如增加html元素的字号,将会导致缓存失效,从而引起整数的布局和重绘。

9、渲染引擎多线程

渲染引擎是单线程的,除了网络操作以外,几乎所有的事情都在单一的线程中处理,在Firefox和Safari中,这是浏览器的主线程,Chrome中这是TAB的主线程。

网络操作由几个并行线程执行,并行连接的个数是受限的(通常是2-6个)。

9.1 事件循环

浏览器主线程是一个事件循环,它被设计为无限循环以保持执行过程的可用,等待事件(例如layout和paint事件)并执行它们。

下面是Firefox的主要事件循环代码:

  1. while (!mExiting)
  2. NS_ProcessNextEvent(thread);

10、CSS2可视化模型

10.1 画布 The Canvas

根据CSS2规范,术语canvas用来描述格式化的结构所渲染的空间——浏览器绘制内容的地方。画布对每个维度空间都是无限大的,但浏览器基于viewport的大小选择了一个初始宽度。

根据http://www.w3.org/TR/CSS2/zindex.html的定义,画布如果是包含在其他画布内则是透明的,否则浏览器会指定一个颜色。

10.2 CSS盒模型

CSS盒模型描述了矩形盒,这些矩形盒是为文档树中的元素生成的,并根据可视的格式化模型进行布局。每个box包括内容区域(如图片、文本等)及可选的四周padding、border和margin区域。

现代浏览器的工作原理

每个节点生成0到n个这样的box。

所有的元素都有一个display属性,用来决定它们生成box的类型,例如:

  • block-生成块状box。

  • inline-生成一个或多个行内box。

  • none-不生成box。

默认的是inline,但浏览器样式表设置了其他默认值,例如,div元素默认为block。可以访问http://www.w3.org/TR/CSS2/sample.html查看更多的默认样式表示例。

10.3 定位策略 Position scheme

这里有三种策略:

  • normal-对象根据它在文档的中位置定位,这意味着它在渲染树和在DOM树中位置一致,并根据它的盒模型和大小进行布局。

  • float-对象先像普通流一样布局,然后尽可能的向左或是向右移动。

  • absolute-对象在渲染树中的位置和Dom树中位置无关。

static和relative是normal,absolute和fixed属于absolute。

在static定位中,不定义位置而使用默认的位置。其他策略中,作者指定位置——top、bottom、left、right。

Box布局的方式由这几项决定:box的类型、box的大小、定位策略及扩展信息(比如图片大小和屏幕尺寸)。

10.4 Box类型

  • Block box:构成一个块,即在浏览器窗口上有自己的矩形

现代浏览器的工作原理

  • Inline box:并没有自己的块状区域,但包含在一个块状区域内

现代浏览器的工作原理

block一个挨着一个垂直格式化,inline则在水平方向上格式化。

现代浏览器的工作原理

Inline盒模型放置在行内或是line box中,每行至少和最高的box一样高,当box以baseline对齐时——即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的box高。当容器宽度不够时,行内元素将被放到多行中,这在一个p元素中经常发生。

现代浏览器的工作原理

10.5 定位 Position

  • Relative

相对定位——先按照一般的定位,然后按所要求的差值移动。

现代浏览器的工作原理

  • Floats

一个浮动的box移动到一行的最左边或是最右边,其余的box围绕在它周围。下面这段html:

  1. <p>
  2. <img style=`float:right` src=`images/image.gif` width=`100″ height=`100″>Lorem ipsum dolor sit amet, consectetuer…
  3. </p>

将显示为:

现代浏览器的工作原理

  • Absolute和Fixed

这种情况下的布局完全不顾普通的文档流,元素不属于文档流的一部分,大小取决于容器。Fixed时,容器为viewport(可视区域)。

现代浏览器的工作原理

注意:fixed即使在文档流滚动时也不会移动。

10.6 Layered representation

这个由CSS属性中的z-index指定,表示盒模型的第三个大小,即在z轴上的位置。Box分发到堆栈中(称为堆栈上下文),每个堆栈中靠后的元素将被较早绘制,栈顶靠前的元素离用户最近,当发生交叠时,将隐藏靠后的元素。堆栈根据z-index属性排序,拥有z-index属性的box形成了一个局部堆栈,viewport有外部堆栈,例如:

  1. <STYLE type=`text/css`>
  2. div {
  3. position: absolute;
  4. left: 2in;
  5. top: 2in;
  6. }
  7. </STYLE>
  8. <P>
  9. <DIV
  10. style=`z-index: 3;background-color:red; width: 1in; height: 1in; `>
  11. </DIV>
  12. <DIV
  13. style=`z-index: 1;background-color:green;width: 2in; height: 2in;`>
  14. </DIV>
  15. </p>

结果是:

现代浏览器的工作原理

虽然绿色div排在红色div后面,可能在正常流中也已经被绘制在后面,但z-index有更高优先级,所以在根box的堆栈中更靠前。

参考资源





posted @ 2016-11-23 21:32  KiteRunner  阅读(490)  评论(0编辑  收藏  举报