浏览器工作机制

本文介绍了浏览器的工作原理,包括主流浏览器、浏览器的主要功能、结构以及组件间的通信。重点讲解了渲染引擎的工作流程,如解析HTML、CSS、JavaScript,构建DOM树和渲染树,布局和绘制过程。同时探讨了渲染引擎的线程和通信机制,以及HTML解析器的运作。通过对这些概念的深入理解,读者可以更好地掌握浏览器如何呈现网页。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

译文

1. 介绍(Introduction)

浏览器可能是使用最广泛的软件。在此我将介绍其屏幕后的工作原理。我们将会看到当我们在地址输入栏中敲入"google.com" 后浏览器将 Google 页面呈现出来的过程中具体发生了什么。

1.1 主流的浏览器(The browsers we talk about)

至今有五种主流的浏览器在使用 :Internet Explorer(IE)/Edge,Firefox,Safari,Chrome 和 Opera。

我将会从开源浏览器 Firefox,Chrome 和部分开源的 Safari 中举一些例子。

根据 W3C 浏览器统计 当前(2009年10月),Firefox,Chrome 和 Safari 总共使用量占比接近总的 60%。

截至翻译日期(2020年2月),光 Chrome 一家就占比80%多。

由此也可见在浏览器业务中开源浏览器是占绝大部分的。

1.2 浏览器的主要功能(The browser’s main functionality)

浏览器的主要功能就是呈现你想要的页面,通过向目标服务器请求页面资源并将响应资源展示在浏览器窗口上。资源格式同来说是 HTML,但也可以是 PDF,image等等。资源的具体地址由 URI(Uniform Resource Identifier) 确定,一般我们常说的 URL 就属于 URI。

浏览器是通过 HTML 和 CSS 规范来解析和展示 HTML 文件的,这些规范由 W3C 组织维护。由于这些年来各个浏览器仅遵循部分规范中的部分内容并开发出各自的一些扩展,给 web 开发者造成了严重的兼容性问题。不过现如今浏览器厂商或多或少的都会遵循这些规范,真心希望能够统一这些规范,不光是为了开发者也是为了用户

各个浏览器的界面大都是类似的,一般界面元素有:

  • 地址栏,用于插入 URI
  • 前进后退按钮
  • 标签选择器
  • 刷新按钮,用于强制刷新或者刷新过程中停止刷新
  • 主页按钮,用于返回主页

浏览器的用户界面是没有正式规范指定的,能形成这种样子,都是通过各个浏览器实践磨合出来的。HTML5 规范也没有规定浏览器必须拥有哪些元素,但是列出了常用的元素,比如地址栏、状态栏和工具栏。当然也有一些特定浏览器独有的特性,比如火狐的下载管理器。

1.3 浏览器的结构(The browser’s high level structure)

  1. 用户界面,包括地址栏,前进/后退按钮,书签菜单栏等。可以说是除了显示网页的窗口外,浏览器的每一部分。
  2. 浏览器引擎:用于查询和操作渲染引擎的接口
  3. 渲染引擎:用于展示请求内容。比如请求的内容是 HTML,它负责解析 HTML 和 CSS 并将解析后的内容展示在页面上。
  4. 网络:用于网络调用,比如 HTTP 请求。它有独立于平台的接口,每个平台都有其对应的底层实现。
  5. UI Backend:用于绘制基本的小部件,比如组合框和窗口。它展示了与平台无关的通用接口。在底层使用的是操作系统用户界面的方法(user interface methods)。
  6. Javascript 解释器:用于解释和执行 Javascript 代码
  7. 数据存储:属于 persistence layer。浏览器需要保存各种数据至硬盘中,比如 cookies,或者 HTML5 规范中的 web database。

图1.
在这里插入图片描述
注意: Chrome 浏览器不像大多数浏览器,它拥有多个渲染引擎实例, 每个 tab 页面都有一个。每个 tab 页面都是一个独立的进程。

1.4 组件之间的通信(Communication between the components)

Firefox 和 Chrome 都开发了独特的通信框架。

2 渲染引擎(the rendering engine)

渲染引擎主要用于将请求资源展示在浏览器中。

浏览器默认是可以展示 HTML 和 XML 格式的资源文件,其余像 PDF 格式的就需要通过插件来支持。在这章,我们只讲主要的事例——展示被CSS 格式化的 HTML 和 images 文件。

2.1 渲染引擎的种类(Rendering engines)

Firefox浏览器用的是 Gecko 渲染引擎,它是一个自制的 Mozilla 渲染引擎。

Safari 和 Chrome 用的都是 Webkit 引擎。

Webkit 引擎是一个开源的渲染引擎,它最开始是在 Linux 平台上,后被苹果公司修改用于支持 Mac 和 Windows 系统。详见 http://webkit.org/

2.2 主要流程(the main flow)

渲染引擎最开始会从网络层获取请求的文档内容,之后渲染引擎的基本流程如下:

图2. 在这里插入图片描述

渲染引擎会线解析 HTML 文件,会将相应的的标签转成 DOM 树中的节点。它将会解析样式数据,包括外部样式和 style 元素内的样式。这些样式信息结合 HTML 中的可视化说明被用于构建另一个树——渲染树(render tree).

这渲染树包含带有像颜色和尺寸等可视化属性的矩形,这些矩形按照正确的顺序显示在屏幕上。

在构造完渲染树后,会进入一个 layout 过程。这意味着为每个节点提供在屏幕上的具体位置坐标。下一阶段就是绘制(painting),渲染树将会被遍历,并且每个节点都会使用 UI backend 层绘制出来。

注: 重要的是要理解这是一个渐进的过程。为了获得更好的用户体验,渲染引擎将会尽快地在屏幕上显示内容。它不会等到所有的 HTML 都被解析后才开始构建和 layout 渲染树。部分内容将被解析和显示,而这个过程将继续处理来自网络的其余内容。

2.3 主要流程实例(Main flow Examples)

Webkit 主要流程如下图:

图3.在这里插入图片描述

Gecko 主要流程如下图:

图4.
在这里插入图片描述

Gecko 把可视格式化元素称为 “Frame tree”,每个元素就一个 “frame”。而 Webkit使用 “Render Tree” 形式并且由 “Render Objects” 组成。Webkit将元素定位操作称为 “layout”,而 Gecko称为 “Reflow”. “attachment” 是 Webkit地术语,为的是连接 DOM 结点和可视化信息来构造渲染树。一个细微非语义的差异在于 Gecko在 HTML 和 DOM 树之间存在一个额外的 “layer”,称之为 “content sink”,主要是生成 DOM 元素。接下来将详细的介绍以上流程的每一部分。

2.4 解析和 DOM 树构造(Parsing and DOM tree construction)

2.4.1 通用的解析方法(Parsing - general)

解析在渲染引擎中是一个非常重要的过程,所以我们将稍微深入地讨论下。

文档解析意味着将其转换成某种合理的结构使得代码能够理解何使用,解析的结果通常是一个能够表示文档结构的树。它称之为 “parse tree”(解析树) 或者 “syntax tree”(语法树)

比如 解析表达式 2+3-1 可以返回以下这个树:

图5.
在这里插入图片描述

语法

解析要基于文档遵循的语法规则——即编写文档时使用的语言或格式。你解析的每种格式都必须有由词汇表和语法规则组成的确定性语法。 这称之为 “context free grammar”, 人类语言不是这样的语言,因此不能用传统的解析技术进行解析。

Parser - Lexer combination

解析可分为词法(lexical)分析和语法(syntax)分析两个子过程。

词法(lexical)分析是将输入分解为 tokens 的过程。tokens 是语言词汇表,有效构建块的集合。在人类语言中,它将由该语言的字典中出现的所有单词组成。

语法(syntax)分析是对语言语法规则的应用。

解析器通常将工作划分为两个组件:“lexer”(有时称为 tokenizer)负责将输入分解为有效的 tokens,而解析器(parser)负责根据语言语法规则分析文档结构来构 造解析树。“lexer” 知道如何去掉不相关的字符,比如空格和换行符。

图6.
在这里插入图片描述

解析过程是迭代的。解析器通常会向 “lexer” 请求一个新的标记(token),并尝试用语法规则来匹配这个标记(token)。如果有规则与之匹配,那么与该标记(token)对应的节点将被添加到解析树中,解析器将请求另一个标记(token)。

如果没有规则可匹配,解析器将在内部存储 token,并不断寻找 tokens,直到找到匹配所有内部存储的 token 的规则。如果没有找到任何规则,则解析器将引发异常。这意味着该文档无效且包含语法错误。

Translation(翻译)

很多时候,解析树并不是最终的结果。解析通常用于翻译——将输入文档转换为另一种格式。编译就是一个例子。将源代码编译成机器码的编译器首先将其解析成解析树,然后将树转换成机器码文档。

图7.
在这里插入图片描述

解析实例

图5中,我们为一个数学表达式建立了解析树。我们尝试定义一个简单的数学语言并理解其解析过程。

词汇:该语言包含整数,加号和减号。

语法规则:

  1. 语言语法的构建块是表达式、术语(term)和操作。
  2. 语言可以包含任何数字的表达。
  3. 表达式被定义为:一个术语(term)后面跟一个“操作”后面跟另一个术语(term)
  4. 操作是正标记(token)或负标记(token)
  5. 术语(term)是整数标记或表达式

接下来分析输入 “2 + 3 - 1”

第一个匹配规则的子串是“2”,根据规则 #5,它是一个术语。第二个匹配项是“2 + 3”,它匹配第二个规则:一个术语后面跟着一个操作,后面跟着另一个术语。下一个匹配将只会在输入结束时命中。“2 + 3 - 1”是一个表达式,因为我们已经知道,2+3 是一个 term(因为根据规则3可知2+3是一个表达式,再根据规则5可知2+3 是一个 term),所以我们有一个 term,然后是一个运算,然后是另一个term。再比如 “2 + +” 将不匹配任何规则,因此是无效的输入。

词汇表和语法的正式定义

词汇通常用利用 regular expressions 来表达。

比如,我们的语言词法可以被定义为:

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

由上可知,整数由正则表达式定义。

语法通常由称为 BNF 的一种格式来定义, 我们的语言语法可以被定义为:

expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression

我们说过,如果一种语言的语法是 “context free grammar”,那么它可以被常规语法分析器解析。“context free grammar” 的直观定义是可以完全用BNF表示的语法。有关正式定义,请参阅 http://en.wikipedia.org/wiki/Context-free_grammar 。

解析器的类型(Types of parsers)

解析器有两种基本类型:自顶向下解析器和自底向上解析器。一个直观的解释是,自顶向下解析器查看语法的高级结构,并尝试匹配其中之一。自底向上解析器从输入开始,逐步将其转换为语法规则,从低级规则开始,直到满足高级规则为止。

让我们看看这两种类型的解析器将如何解析我们的例子:

自顶向下解析器将从高级规则开始—它将“2 + 3”标识为表达式。然后,它将“2 + 3 - 1”标识为一个表达式(标识表达式的过程演化为匹配其他规则,但起点是最高级别的规则)。

自底向上解析器将扫描输入,直到匹配规则为止,然后用规则替换匹配的输入。这将一直持续到输入结束。部分匹配的表达式放在解析器堆栈上:

stackInput
2 + 3 - 1
term+ 3 - 1
term operation3 - 1
expression- 1
expression operation1
expression

这种自底向上的解析器称为"shift reduce"解析器,因为输入被移向右边(假设一个指针首先指向输入起点,然后向右移动),并逐渐简化为语法规则。

自动生成解析器(Generating parsers automatically)

有一些工具可以为你生成解析器。它们被称为解析器生成器。你向它们提供你的语言语法—词汇表和语法规则,它们就会生成一个有效的解析器。创建解析器需要对解析有深刻的理解,手动创建很好的解析器并不容易,因此解析器生成器非常有用。
Webkit使用了两个众所周知的解析器生成器,Flex用于创建"lexer", Bison用于创建解析器(你可能会遇到名称为 Lex 和 Yacc 的解析器)。Flex输入是一个包含由标记(tokens) 正则表达式定义的文件。 Bison的输入是 BNF 格式的语言语法规则。

2.4.2 HTML 解析器(HTML Parser)

HTML 解析器的工作是将 HTML 标记解析成解析树。

HTML 语法定义(The HTML grammar definition)

HTML的词汇表和语法是在 w3c 组织创建的规范中定义的。当前版本是HTML4, HTML5的工作正在进行中。

Not a context free grammar

正如我们在解析介绍中看到的,语法可以使用 BNF 之类的格式来正式定义。

不幸的是,所有传统的解析器都不适用于HTML(我并不是为了好玩才提出这些的,它们将用于解析CSS和JavaScript)。解析器需要的 context free grammar 很难定义HTML。

有一种定义HTML - DTD(文档类型定义, Document Type Definition)的正式格式,但它不是 context free grammar。

这看起来很奇怪,HTML 非常接近 XML,有很多可用的 XML 解析器。HTML有一个XML变体——XHTML,那么最大的区别是什么呢?

不同之处在于HTML方法更“宽容”,它允许您省略某些隐式添加的标记,有时省略标记的开始或结束等。总的来说,它是一种“软”语法,而不是 XML 僵硬和苛刻的语法。

很明显,这种看似微小的差异会造成天壤之别。一方面,这是为什么 HTML 如此流行的主要原因——它原谅你的错误,利于web开发者。另一方面,它使编写格式语法变得困难。总而言之,HTML不容易解析,传统解析器无法解析它,因为它的语法不是 context free grammar,XML解析器也无法解析它。

HTML DTD

HTML定义是DTD格式。这种格式用于定义 SGML 家族的语言。该格式包含所有允许的元素及其属性和层次结构的定义。正如我们前面看到的,HTML DTD不能形成 context free grammar。

DTD有几个变体。严格模式仅符合规范,但其他模式包含对过去浏览器使用的标记的支持。其目的是向后兼容旧的内容。当前的严格DTD在这里:http://www.w3.org/TR/html4/strict.dtd

DOM

作为输出树,本质上是拥有DOM元素和属性节点的解析树。DOM是(文档对象模型,Document Object Model)的简称。它用于表示HTML文档的对象,是HTML元素与外部世界(如JavaScript)的接口。其中DOM树的根是“Document”对象。

DOM与标记几乎是一对一的关系。比如:

<html>
	<body>
		<p>
			Hello World
		</p>
		<div> <img src="example.png"/></div>
	</body>
</html>

将被转换为以下的 DOM 树:

图8.
在这里插入图片描述

与HTML一样,DOM也是由w3c组织指定的。见http://www.w3.org/DOM/DOMTR。它是操作文档的通用规范。其中特定的模块描述HTML对应的元素。HTML定义可以在这里找到:http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。

当我说树包含DOM节点时,我的意思就是这树是由实现一个DOM接口的元素构成的。浏览器也会实现某些在浏览器内部使用的其他属性。

解析算法(The parsing algorithm)

正如我们在前面几节中看到的,不能使用常规的自顶向下或自底向上解析器解析HTML。

原因如下:

  1. 语言的宽容本性
  2. 浏览器具有传统的容错能力来支持众所周知的无效HTML。
  3. 可重入(reentrant)的解析过程。通常在解析过程中源代码不会改变,但是在HTML中,脚本标记包含“document.write“可以添加额外的标记(tokens),因此解析过程实际上修改了输入.

由于无法使用常规的解析技术,浏览器创建自定义解析器来解析HTML。

解析算法由HTML5规范详细描述。该算法由 tokenization 和 树构造(tree construction) 两个阶段组成。

tokenization 是词法分析,将输入解析为标记(tokens)。在HTML标记(tokens)中有开始标签、结束标签、属性名称和属性值。

tokenization 识别标记(token),将其交给树构造器(tree constructor),并使用下一个字符来识别下一个标记,以此类推,直到输入结束。

在这里插入图片描述

The tokenization algorithm

算法的输出是一个HTML标记(token)。该算法被表示为一个状态机。每个状态使用输入流的一个或多个字符,并根据这些字符更新下一个状态。决策受到当前 tokenization 状态和树构造(tree construction) 状态的影响。这意味着,根据当前状态,相同的已使用字符将为正确的下一个状态产生不同的结果。这个算法太复杂了,所以我们来看一个简单的例子来帮助我们理解原理。

比如:

<html>
	<body>
		Hello world
	</body>
</html>

初始状态是“数据状态(Data state)”。当遇到 “<” 字符时,状态变为 “标签打开状态(Tag open state)”。使用 “a-z” 字符会创建 “开始标签标记(Start tag token)”,状态将更改为“标签名称状态(Tag name state)”。我们将一直保持这种状态,直到“>”字符被使用。每个字符都附加到这新的 token 中。在我们的例子中,创建的token是一个“html” 标记(token)。

当遇到“>”字符时,当前 token 被触发并将状态更改回“数据状态(Data state)”。<body>标签将按照相同的步骤处理。到目前为止,已经处理了“html”和“body”标签。我们现在回到了“数据状态(Data state)”。使用“Hello world”的“H”字符将导致创建和触发字符标记(token),这将一直进行下去,直到遇到“</body>”的“<”为止。我们将为“Hello world”的每个字符发出一个字符标记(token)。

我们现在回到了“标签打开状态(Tag open state)”。使用下一个输入“/”将导致创建“结束标签标记(end tag token)”并更新为“标记名称状态(Tag name state)”。我们再次保持这种状态,直至遇到“>”。然后发出新的标签token,并返回“数据状态(Data state)”。“</html>”输入将像前面的情况一样处理。

图9.在这里插入图片描述

构建树算法(Tree construction algorithm)

创建解析器时,将创建文档对象。在树构建阶段,根文档的DOM树将被修改,元素将被添加到其中。记号赋予器(tokenizer)发出的每个节点都将由树构造函数处理。对于每个 token,规范定义了与它相关的DOM元素,并将为此 token 创建。除了将元素添加到DOM树之外,它还被添加到开放元素的栈中。此栈用于纠正嵌套未匹配和未关闭的标签。该算法也被描述为一个状态机。这些状态称为“插入模式”。比如:

<html>
	<body>
		Hello world
	</body>
</html>

树构造阶段的输入是来自tokenization阶段的一系列 tokens,第一种模式是“初始模式(initial mode)”。接收 html token 后将会移动到“before html”模式,并在该模式中重新处理 token。这将导致 HTMLHtmlElement 元素的创建,它将被附加到根文档对象。

状态将更改为“before head”。我们接收“body”令牌。HTMLHeadElement将被隐式创建,尽管我们没有“head”标记,它将被添加到树中。

我们现在进入“in head”模式,然后进入“after head”模式。对 body 标记(token)进行重新处理,创建并插入一个HTMLBodyElement 元素,并将模式更新为“in body”。

现在接收到“Hello world”字符串的字符标记(token)。第一个字符将会创建和插入一个“Text”节点,其他字符将追加到该节点。

接收body结束token后将会更新为“after body”模式。现在我们将接收到html结束标签,它将把我们移动到“after after body”模式。接收文件标记的结束将结束解析。

图10.在这里插入图片描述
Actions when the parsing is finished

在这个阶段,浏览器将把文档标记为交互式的,并开始解析处于“延迟”模式的脚本,这些脚本应该在解析文档之后执行。然后将文档状态设置为“完成”,并触发“加载”事件。

你可以在HTML5规范中看到标记化和树形结构的完整算法 http://www.w3.org/TR/html5/syntax.html#html-parser

Browsers error tolerance

在HTML页面上永远不会出现“无效语法”错误。浏览器修复了无效的内容,然后继续。以这个HTML为例:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
  	Really lousy HTML
  </p>
</html>

我肯定违反了很多个规则(“mytag”不是一个标准的标签,“p”和“div”元素嵌套错误等等),但是浏览器仍然正确地显示它,并且没有任何抱怨。因此,很多解析器代码都在修复HTML开发者的错误。

错误处理在浏览器中是相当一致的,但令人惊讶的是,它不是HTML当前规范的一部分。就像书签和后退/前进按钮一样,它只是多年来在浏览器中开发出来的东西。已知有无效的HTML结构在许多站点中重复出现,浏览器试图以与其他浏览器一致的方式修复它们。

HTML5规范确实定义了其中一些需求。Webkit在HTML解析器类开头的注释中很好地总结了这一点

The parser parses tokenized input into the document, building up the document tree. If the document is well-formed, parsing it is straightforward.

Unfortunately, we have to handle many HTML documents that are not well-formed, so the parser has to be tolerant about errors.

We have to take care of at least the following error conditions:

1. The element being added is explicitly forbidden inside some outer tag.
In this case we should close all tags up to the one, which forbids the element, and add it afterwards.

2. We are not allowed to add the element directly. 
It could be that the person writing the document forgot some tag in between (or that the tag in between is optional).
This could be the case with the following tags: HTML HEAD BODY TBODY TR TD LI (did I forget any?).

3. We want to add a block element inside to an inline element. Close all inline elements up to the next higher block element.

4. If this doesn't help, close elements until we are allowed to add the element or ignore the tag.

让我们看看一些 Webkit 容错的例子

1.</br> instead of <br>

有些网站使用 </br> 替代 <br>。为了兼容 IE 和 Firefox,Webkit 将其当作 <br>,代码如下:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     reportError(MalformedBRError);
     t->beginTag = true;
}

注: 错误处理是在内部进行的,不会展示给用户。

2.再比如:

<table>
	<table>
		<tr><td>inner table</td></tr>
         </table>
	<tr><td>outer table</td></tr>
</table>

Webkit 会将把层次结构改为两个兄弟表:

<table>
	<tr><td>outer table</td></tr>
</table>
<table>
	<tr><td>inner table</td></tr>
 </table>

处理代码如下:

if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);

Webkit 为当前元素内容使用了一个栈,它将把内部表从外部表栈中弹出。这些表现在成为了兄弟表。

3.嵌套的表单元素

如果用户将一个表单放入另一个表单中,则忽略第二个表单。

处理代码如下:

if (!m_currentFormElement) {
        m_currentFormElement = new HTMLFormElement(formTag,    m_document);
}

3.太深的标记层次结构

以下注释就已经说明了一切

/*
www.liceo.edu.mx is an example of a site that achieves a level of nesting of about 1500 tags, all from a bunch of <b>s.
We will only allow at most 20 nested tags of the same type before just ignoring them all together.
*/
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}

5.错位的html或主体结束标记

以下注释也足以说明情况

/*
Support for really broken html.
We never close the body tag, since some stupid web pages close it before the actual end of the doc.
Let's rely on the end() call to close things.
*/
if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

另外,web开发者需注意:除非你想以 Webkit 容错代码为例,其余情况请编写格式良好的HTML

2.4.3 CSS解析(parsing)

还记得介绍中的解析概念吗?与HTML不同,CSS是一种 context free grammar ,可以使用介绍中描述的解析器类型进行解析。事实上,CSS规范定义了CSS的词法和语法语法.

来看些例子:

词汇语法(词汇表)中每个标记(token)的正则表达式定义:

comment		\/\*[^*]*\*+([^/*][^*]*\*+)*\/
num			[0-9]+|[0-9]*"."[0-9]+
nonascii	[\200-\377]
nmstart		[_a-z]|{nonascii}|{escape}
nmchar		[_a-z0-9-]|{nonascii}|{escape}
name		{nmchar}+
ident		{nmstart}{nmchar}*

“ident” 是标识符(identifier)的缩写,类似于类名。“name”是一个元素id(由“#”引用)

语法由 BNF 描述

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator selector ] ]
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

解释: 规则集(ruleset)是这样的结构:

div.error , a.error {
	color:red;
	font-weight:bold;
}

div.errora.error 是选择器。 花括号内的部分包含此规则集应用的规则。 这个结构的正式定义如下:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

这意味着规则集是一个选择器,或者是由逗号和空格分隔的可选选择器数量(S代表空格)。规则集包含花括号,其中包含一个声明或多个声明,声明之间用分号分隔。“声明”和“选择器”将在下面的BNF定义中定义。

Webkit CSS parser

Webkit 使用 Flex和Bison 解析器生成器从 CSS 语法文件中自动创建解析器。正如在解析器简介中所回忆的,Bison创建了一个自底向上的 shift reduce 解析器。Firefox 使用手动编写的自顶向下解析器。在这两种情况下,每个CSS文件都被解析成一个样式表对象,每个对象都包含CSS规则。CSS规则对象包含选择器和声明对象以及其他与CSS语法相对应的对象。

在这里插入图片描述

2.4.4 JavaScript解析(Parsing scripts)
2.4.5 执行脚本和样式表的顺序(The order of processing scripts and style sheets)

Scripts

web 的模型是同步的。作者希望在解析器遇到<script>标签时立即解析和执行脚本。文档的解析将暂停,直到执行脚本为止。如果脚本是外部的,那么必须首先从网络获取资源,这也是同步完成的,解析将暂停,直到获取资源。这是多年的模型,也在HTML 4和HTML 5规范中指定。作者可以将脚本标记为“defer”,这样它就不会停止文档解析,而是在解析之后执行。HTML5增加了一个将脚本标记为异步的选项,这样它将被另一个线程解析和执行。

推测性解析(Speculative parsing)

Webkit和Firefox 都做了这种优化。在执行脚本时,另一个线程解析文档的其余部分,并找出需要从网络加载哪些其他资源并加载它们。这些方式可以在并行连接上将资源加载,并且总体速度更好。注意:推测性解析器不修改DOM树,而将其保留给主解析器,它只解析对外部资源的引用,如外部脚本、样式表和图像。

Style sheets

另一方面,样式表有一个不同的模型。从概念上看,由于样式表不会更改DOM树,所以没有理由等待它们并停止文档解析。不过,在文档解析阶段存在脚本请求样式信息的问题。如果还没有加载和解析样式,脚本将得到错误的结果,这显然会导致很多问题。这似乎是一种极端情况,但很常见。当样式表仍然在加载和解析时,Firefox会阻塞所有脚本。Webkit只在脚本试图访问某些可能受未加载的样式表影响的样式属性时阻塞脚本。

2.5 渲染树的构建(Render tree construction)

在构建DOM树时,浏览器将构建另一个树,即渲染树。这个树由将会被顺序显示的可视元素组成。它是文档的可视化表示。此树的目的是使内容能够按照正确的顺序进行绘制。

Firefox将渲染树中的元素称为“框架(frames)”。Webkit使用术语 renderer 或render 对象。renderer 知道如何布局和绘制它自己和它的子元素。

webkit RenderObject类,renderer 的基类有如下定义:

class RenderObject{
	virtual void layout();
	virtual void paint(PaintInfo);
	virtual void rect repaintRect();
	Node* node;  //the DOM node
	RenderStyle* style;  // the computed style
	RenderLayer* containgLayer; //the containing z-index layer
}

每个 renderer 代表一个矩形区域,通常对应于节点的CSS框,如CSS2规范所述。它包含几何信息,如宽度、高度和位置。

盒类型(box type)受到与节点相关的“display”样式属性的影响(请参阅样式计算一节)。下面是Webkit代码,用于根据display属性决定应该为DOM节点创建哪种类型的 renderer。

RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
    Document* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    RenderObject* o = 0;

    switch (style->display()) {
        case NONE:
            break;
        case INLINE:
            o = new (arena) RenderInline(node);
            break;
        case BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case INLINE_BLOCK:
            o = new (arena) RenderBlock(node);
            break;
        case LIST_ITEM:
            o = new (arena) RenderListItem(node);
            break;
       ...
    }

    return o;
}

元素类型也要考虑,例如表单控件和表格有特殊的 frames。在 Webkit 中,如果一个元素想要创建一个特殊的 renderer ,它会覆盖“createRenderer”方法。 renderer 指向包含非几何信息的样式对象。

2.5.1 渲染树与DOM树的关系(The render tree relation to the DOM tree)

renderer 对应于DOM元素,但关系不是一对一的。不可视的DOM元素不会插入到渲染树中。一比如“head”元素。同样,将display属性赋值为“none”的元素将不会出现在树中(具有“hidden”可视性属性的元素将出现在树中)。

也会存在DOM元素对应于几个可视对象的情况。这些元素通常具有复杂的结构,不能用单个矩形来描述。例如,“select”元素有3个渲染器——一个用于显示区域,一个用于下拉列表框,一个用于按钮。此外,当文本被分成多行,因为宽度不够一行,新的行将作为额外的 renderer 添加。

另一个 renderer 的例子是 broken HTML。根据CSS规范,内联(inline)元素必须只包含块(block)元素或只包含内联(inline)元素。在混合内容(mixed content)的情况下,将创建匿名块 renderer 来包装内联元素。

有些呈现对象(render objects)对应于DOM节点,但不在树中的相同位置。浮动和绝对定位元素脱离了文档流,放置在树中的不同位置,并映射到实际帧(real frame)。占位符 frame 是它们应该在的位置。

图11.
在这里插入图片描述

2.5.2 树的构建流程(The flow of constructing the tree)

在Firefox中,the presentation 注册为一个侦听器用于DOM更新。
the presentation 将框架(frame)的创建委托给“FrameConstructor”, constructor 解析样式(请参阅样式计算)并创建框架(frame)。

在Webkit中,解析样式并创建 renderer 的过程称为“attachment”。每个DOM节点都有一个“attach”方法。“attachment” 是同步的,节点插入到DOM树会调用新的节点“attach”方法。

处理html和body标记将会构造渲染树的根。
根渲染对象(root render object)对应于CSS规范所称的包含块(containing block),是包含其他所有块的最上面的块。它的尺寸是viewport,浏览器窗口显示区域的尺寸。Firefox称它为 ViewPortFrame, Webkit称它为RenderView。这是文档所指向的 render object 。树的其余部分构造为DOM节点插入。
关于这个主题,请参见CSS2—http://www.w3.org/TR/CSS21/intro.html#processing-model。

2.5.3 样式计算(Style Computation)

构建渲染树需要计算每个渲染对象(render object)的可视属性。这是通过计算每个元素的样式属性来实现的。

这样式包括各种来源的样式表、内联样式元素和HTML中的可视属性(如“bgcolor”属性)。后者被转换成与之匹配的CSS样式属性。

样式表的起源是浏览器的默认样式表、由页面开发者提供的样式表和用户样式表,这些用户样式表是由浏览器用户提供的样式表(浏览器允许你定义自己喜欢的样式。例如,在Firefox中,这是通过在"Firefox Profile”文件夹中放置一个样式表来实现的)。

样式计算也带来了一些困难:

  1. 样式数据是一个非常大的结构,包含许多样式属性,这可能会导致内存问题。

  2. 如果没有优化,为每个元素查找匹配规则可能会导致性能问题。为每个元素遍历整个规则列表以查找匹配项是一项繁重的任务。选择器可能具有复杂的结构,这可能导致匹配过程从一条看起来很有希望的路径开始,但这条路径最后被证明是无效的,又必须得尝试另一条路径。
    例如,这个复合选择器:

    div div div div{
    ...
    }
    

    表示规则适用于“<div>”,它是3个div的后代。假设您想要检查该规则是否适用于给定的“<div>”元素。您选择树中的特定路径进行检查。您可能需要遍历节点树,却发现只有两个div,导致规则不适用。然后又需要尝试树中的其他路径。

  3. 在定义规则层次结构时,应用规则会涉及相当复杂的级联规则

让我们看看浏览器如何面对这些问题:

1).共享样式数据(Sharing style data)

Webkit 节点引用样式对象(RenderStyle),这些对象在某些情况下可以被节点共享。节点是兄弟姐妹或表兄妹:

  1. 元素必须处于相同的鼠标状态(mouse state)(例如,当一个元素不处于:hover状态时,另一个也不能处于:hover状态)。
  2. 两个元素都不应该有id
  3. 标签名称应该匹配
  4. 类属性应该匹配
  5. 映射的属性集必须是相同的
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 这两个元素都不应该受属性选择器的影响,其中受影响的定义为具有在属性选择器中的任何位置都使用一个属性选择器的任何选择器匹配项
  9. 元素上必须没有内联样式属性
  10. 必须完全不使用兄弟选择器。当遇到任何兄弟选择器时,WebCore只会抛出一个全局开关(switch),当它们出现时,会禁用整个文档的样式共享。这包括+选择器和向:first-child和:last-child这样的选择器。

2) 火狐规则树( Firefox rule tree )

Firefox有两个额外的树来简化样式计算——规则树(rule tree)和样式上下文树(style contexts tree)。Webkit也有样式对象,但它们不像样式上下文树那样存储在树中,只有DOM节点指向它的相关样式。

图13.
在这里插入图片描述

样式上下文包含结束值(end values)。这些值是通过以正确的顺序应用所有匹配规则并执行将它们从逻辑值转换为具体值的操作来计算的。例如,如果逻辑值是屏幕的百分比,它将被计算并转换为绝对单位。规则树的想法很聪明。它允许在节点之间共享这些值,以避免再次计算它们。这也节省了空间。

所有匹配的规则都存储在树中。路径中的底层节点具有更高的优先级。该树包含找到的规则匹配的所有路径。存储规则是惰性的。树并不是在开始时为每个节点计算,只有每当需要计算节点样式时,才会将计算过的路径添加到树中。

其思想是将树路径视为词汇库中的单词。假设我们已经计算了这个规则树:

在这里插入图片描述

假设我们需要为content tree中另一个元素匹配规则,并找到了匹配的规则(以正确的顺序)是B - E -I。在树中我们已经有了这条路径,因为我们已经计算路径A - B - E - I - L .现在我们就无需做更多的工作。

让我们看看树是如何工作的。

分解成structs(Division into structs)

样式上下文被划分为结构(structs)。这些结构包含特定类别(如边框或颜色)的样式信息。结构中的所有属性要么是继承的,要么是非继承的。继承属性是指除非由元素定义,否则从其父元素继承的属性。非继承属性(称为“重置”属性)如果没有定义,则使用默认值。

这树有利于将整个结构(包含计算的最终值)缓存到树中。其思想是,如果底层节点不提供结构的定义,则可以使用上层节点中缓存的结构。

使用规则树计算样式上下文(Computing the style contexts using the rule tree)

当计算某个元素的样式上下文时,我们首先计算规则树中的路径或使用现有的路径。然后,我们开始应用路径中的规则来填充新样式上下文中的结构。我们从路径的底部节点开始,它是具有最高优先级的节点(通常是最特定的选择器),然后遍历树,直到我们的结构被填满。如果在那个规则节点中没有对应结构的规范,那么我们可以进行极大的优化,我们向上查找树,直到找到一个完全指定它并简单地指向它的节点,这是最好的优化,整个结构是共享的。这节省了最终值(end values)和内存的计算。

如果我们找到了部分定义,我们就会向上查找树,直到结构被填满。

如果我们没有找到结构体的任何定义,那么在结构体是“继承”类型的情况下,我们在上下文树中指向父结构体,在这种情况下,我们也能成功地共享了结构体。如果它是一个重置结构,那么将使用默认值。

如果最特定的节点确实添加了值,那么我们需要做一些额外的计算来将其转换为实际的值。然后,我们将结果缓存到树节点中,以便子节点可以使用它。

如果元素具有指向同一树节点的兄弟元素,则可以在它们之间共享整个样式上下文。

让我们来看一个例子:假设我们有这个HTML

<html>
	<body>
		<div class="err" id="div1">
			<p>
                          this is a <span class="big"> big error </span>
                          this is also a
                          <span class="big"> very  big  error</span> error
        		</p>
		</div>
		<div class="err" id="div2">another error</div>
    	</body>
</html>

然后添加以下规则:

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

为了简化问题假设我们只需要填入两个结构体(struct):颜色结构体和外边距(margin)结构体。颜色结构体只包含一个成员(即颜色),外边距结构体包含四个方面。

规则树的最终结果看起来是这样的(标记的节点名 : 规则集中的序号):

图12.
在这里插入图片描述

上下文树将会是这样(标签名 : 标记的节点名):

图13.

在这里插入图片描述

假设我们解析HTML并得到第二个<div>标记。我们需要为这个节点创建一个样式上下文并填充它的样式结构。我们将匹配这些规则,并发现<div>的匹配规则是1、2和6。这意味着树中已经有一个元素可以使用的现有路径,我们只需要为规则6添加另一个节点(规则树中的节点F)。我们将创建一个样式上下文并将其放到上下文树中。新的样式上下文将指向规则树中的节点F。

我们现在需要填充样式结构。我们将从填充外边距开始。由于最后一个规则节点(F)没有添加到边距结构中,所以我们可以在树中向上移动,直到找到在上一个节点计算出来的缓存结构并使用它。我们将在节点B上找到它,它是指定外边距规则的最上面的节点。

我们有一个颜色结构的定义,所以我们不能使用一个缓存的结构。因为color有一个属性,所以我们不需要向上查找去填充其他属性。我们将计算结束值(将字符串转换为RGB等)并将计算的结构缓存在这个节点上。

第二个<span>元素的工作甚至更简单。我们将匹配规则,并得出结论,它指向规则G,就像前面的span。因为我们有指向相同节点的兄弟节点,所以我们可以共享整个样式上下文,并且只指向上一个span的上下文。

对于包含从父级继承的规则的结构,缓存是在上下文树中进行的(color属性实际上是继承的,但是Firefox将其视为重置,并将其缓存到规则树中)。
例如,如果我们在段落中添加字体规则:

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

然后,作为上下文树中段落的一个子元素的div元素可以与其父元素共享相同的字体结构。如果没有为“div”指定字体规则,就会出现这种情况。

在Webkit中,若没有规则树,匹配的声明将被遍历4次。首先应用不重要的高优先级属性(应该首先应用的属性,因为其他属性依赖于它们,比如display),然后应用高优先级重要的属性,然后是正常优先级不重要的属性,然后是正常优先级重要的规则。这意味着出现多次的属性将根据正确的级联顺序进行解析。
最后的胜利。

总之,共享样式对象(全部或部分结构)解决了问题1和问题3。Firefox规则树还有助于以正确的顺序应用属性。

3) 为了易于匹配而操作规则(Manipulating the rules for an easy match)

样式规则的一些来源:

  • CSS规则,不管是在外部样式表中还是在样式元素中 p{color:blue}
  • 内联样式属性,<p style="color:blue" />
  • HTML 可视属性,<p bgcolor="blue" />

后两个很容易与元素匹配,因为他拥有样式属性,可以使用元素作为键来映射HTML属性。

正如前面在问题2中提到的,CSS规则的匹配可能比较复杂。为了解决这个问题,对规则进行了操作,以便于访问。

根据选择器,在解析样式表之后,将添加几个散列映射中的一个规则。有按id、按类名、按标签名的映射,还有不属于这些类别的任何内容的通用映射。如果选择器是一个id,规则将被添加到id映射中,如果它是一个类,它将被添加到类映射中,等等。这种操作使匹配规则变得更加容易。没有必要查看每个声明—我们可以从映射中提取元素的相关规则。这种优化消除了95%以上的规则,因此在匹配过程中甚至不需要考虑这些规则(4.1)。

让我们看看下面的风格规则:

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

第一个规则将被插入到类映射中。第二个进入id映射,第三个进入标签映射。
浏览以下HTML片段;

<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>

我们首先尝试找出 p 元素的规则。类映射将包含一个“error”键,该键下是 “p.error” 规则。div元素将在id映射(键是id)和标签映射中具有相关的规则。所以剩下的工作就是找出由键提取的规则中哪些是真正匹配的。

例如,如果div的规则是

table div {margin:5px}

它仍然会从标签映射中提取出来,因为键是最右边的选择器,但是它与没有表祖先的div元素不匹配。

Webkit和Firefox都做了这个操作。

4) 以正确的级联顺序应用规则(Applying the rules in the correct cascade order)

style对象具有与每个可视属性(所有css属性,但更通用)对应的属性。如果属性没有由任何匹配的规则定义,那么一些属性可以从父元素样式对象中继承。其他属性有默认值。

当有多个定义时,问题就开始了,这里是解决问题的级联顺序。

1 样式表级联 顺序(Style sheet cascade order)

样式属性的声明可以出现在多个样式表中,也可以在一个样式表中出现多次。这意味着应用规则的顺序非常重要。这就是所谓的“级联”顺序。根据CSS2规范,级联顺序为(从低到高):

  • 浏览器声明
  • 用户正常的声明
  • 开发者正常的声明
  • 开发者主要声明
  • 用户重要声明

浏览器声明是最不重要的,只有在声明被标记为重要时,用户才会覆盖开发者声明。具有相同顺序的声明将按 specifity 排序,然后按指定的顺序排序。HTML可视属性被转换成相匹配的CSS声明。它们被视为低优先级的作者规则。

2 Specifity

选择器的 specifity 由 CSS2规范 定义如下:

  • 如果声明来自’style’属性而不是带有选择器的规则,则计数1,否则为0 (= a)
  • 计算选择器中ID属性的数量(= b)
  • 计算选择器中其他属性和伪类的数量(= c)
  • 计算选择器中元素名称和伪元素的数量(= d)

将这4个数字a-b-c-d(在一个基数很大的数字系统中)连接起来,可以得到 specifity。

你需要使用的数字基数是由你在其中一个类别中拥有的最高计数来定义的。
例如,如果a=14,可以使用十六进制基数。在不太可能的情况下,当a=17时,你需要一个17位数的基数。后面的情况可能会发生与选择器像这样:html主体div div p…(17个标签在你的选择器…不是很有可能)。

比如:

 *             {}  /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
 li            {}  /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
 li:first-line {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul li         {}  /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
 ul ol+li      {}  /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
 h1 + *[rel=up]{}  /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
 ul ol li.red  {}  /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
 li.red.level  {}  /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
 #x34y         {}  /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
 style=""          /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */

3 规则排序(Sorting the rules)

匹配规则后,根据级联规则对它们进行排序。Webkit对小列表使用冒泡排序,对大列表使用归并排序。Webkit通过重写规则的“>”操作符来实现排序:

static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; 
}
2.5.4 渐进过程 (Gradual process)

Webkit 使用一个标记(flag)来标记是否已经加载了所有顶级样式表(包括@imports)。如果在使用附加位置容器时没有完全加载样式,并且在文档中对其进行了标记,则在加载样式表时将重新计算它们。

2.6 布局(layout)

当 renderer(渲染器) 被创建并添加到树中时,它没有位置和大小。计算这些值称为布局(layout)或回流(reflow)。

HTML使用基于流的布局模型,这意味着在大多数情况下,可以在一次遍历中计算几何图形。在流中后面的元素“通常不会影响较早的元素的几何形状”,因此布局可以从左到右、从上到下遍历文档。也有例外——例如,HTML tables可能不止一次。

坐标系是相对于root frame的。使用顶部和左侧坐标。

布局是一个递归过程。它从根 renderer 开始,根 renderer 对应于HTML文档的元素。布局递归地继续通过一些或所有的框架层次结构,为每个需要它的 renderer 计算几何信息。

根 renderer 的位置是0,0,它的尺寸是viewport,浏览器窗口的可见部分。所有 renderer 都有一个“layout”或“reflow”方法,每个 renderer 都调用需要layout 的子渲染器的 layout 方法。

2.6.1 Dirty bit system

为了不给每一个小的变化做一个完整的布局,浏览器使用一个“Dirty bit”系统。一个被改变或添加的 renderer 把它自己和它的子 renderer 标记为“dirty”——需要布局。

有两面flags——“dirty”和“children are dirty”。子元素是脏的意思是,尽管 renderer 本身可能没问题,但它至少有一个需要布局的子元素。

2.6.2 全局和增量布局(Global and incremental layout)

布局可以触发整个渲染树,这是“全局”布局。这可能是由于:

  • 影响所有 renderer 的全局样式更改,如字体大小更改。
  • 屏幕大小调整的结果

布局可以是增量的,只有“dirty”的 renderers 将被布局(这可能会造成一些损害,这将需要额外的布局)。

当 renderers 是“dirty”的时候,增量布局被触发(异步)。例如当来自网络的额外内容被添加到DOM树后,新的渲染器被附加到渲染树中。

图20.
在这里插入图片描述

2.6.3 异步和同步布局(Asynchronous and Synchronous layout)

增量布局是异步完成的。Firefox对增量布局的“reflow命令”进行排队,调度器触发批处理执行这些命令。Webkit也有一个计时器来执行增量布局——树被遍历,“dirty”渲染器被布局出来。

请求样式信息(如“offsightHeight”)的脚本可以同步触发增量布局。

全局布局通常会同步触发。有时布局会在初始布局之后作为回调被触发,因为某些属性(如滚动位置)发生了更改。

2.6.4 优化(Optimizations)

当布局因“resize”或渲染器位置的改变(而不是大小)触发时,渲染大小将从缓存中获取,而不是重新计算。
在某些情况下,只有子树被修改,布局不是从根开始的。当更改是局部的,并且不影响其周围环境时,就会发生这种情况,例如文本插入文本字段(否则每个击键都会触发从根开始的布局)。

2.6.5 布局的过程(The layout process)

布局通常拥有以下的形式:

  1. 父 renderer 决定自己的宽度。
  2. 父 renderer 越过孩子并且:
    1. 放置子 renderer (设置它的x和y)。
    2. 如果需要,调用子布局(它们是脏的,或者我们在全局布局中,或者其他原因)——这将计算子布局的高度。
  3. 父类使用子类的累计高度,外边距(margin)和内边距(padding)的高度来设置它自己的高度,这将被父类渲染器的父类使用。
  4. 设置它的脏位为false。

Firefox使用“state”对象(nsHTMLReflowState)作为布局的参数(称为“reflow”)。其中,“state”包括父母宽度。Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它将包含渲染器计算的高度。

2.6.6 宽度计算(Width calculation)

渲染器的宽度使用容器块的宽度、渲染器的样式“宽度”属性、边距和边框来计算。例如以下div的宽度:<div style="width:30%"/>

Webkit计算如下(类RenderBox方法calcWidth):

  • 容器宽度等于可用容器宽度和0两者的最大值。在这种情况下,availableWidth等于contentWidth,计算如下: clientWidth() - paddingLeft() - paddingRight() , clientWidthclientHeight 表示除边框和滚动条外的对象的内部。
  • 元素宽度是“width”样式属性。它将通过计算容器宽度的百分比计算为绝对值。
  • 现在添加了水平边框(border)和内边距(padding)。

到目前为止,这只是“referred width”的计算。现在将计算最小和最大宽度。
如果 preferred width 大于最大宽度,则使用最大宽度。如果低于最低宽度,则使用最小宽度(最小的不可分裂单元)。

如果需要布局但宽度不变,则缓存这些值。

2.6.7 Line Breaking

当渲染器(renderer)在布局的中间决定它需要打破。它停止并传播到它需要被破坏的父对象。父对象将创建额外的渲染器并在其上调用布局。

2.7 绘制(Painting)

在绘制阶段,遍历渲染树并调用渲染器“paint”方法在屏幕上显示它们的内容。绘画使用UI基础结构组件。关于UI的那一章有更多内容。

2.7.1 全局和增量(Global and Incremental)

和布局一样,绘制也可以是全局的(整个树都被绘制),也可以是增量的。在增量绘制中,一些渲染器(renderer)以一种不影响整个树的方式改变。修改后的渲染器使它在屏幕上的矩形无效。这将导致操作系统将其视为一个“脏区域(dirty region)”,并生成一个“paint”事件。操作系统做得很聪明,把几个区域合并成一个。在Chrome中,它更复杂,因为渲染器是在一个不同的进程,然后是主进程。 Chrome在一定程度上模拟了操作系统的行为。表示侦听这些事件并将消息委托给render root。遍历树,直到遇到相关的渲染器。它会重新绘制自己(通常是它的孩子)。

2.7.2 绘制顺序(The painting order)

CSS2定义了绘制过程的顺序:http://www.w3.org/TR/CSS21/zindex.html。这实际上是元素在上下文中栈的顺序。这个顺序会影响绘制,因为这栈是由后向前绘制的。块渲染器的栈顺序为:

  1. 背景色(background color)
  2. 背景图片(background image)
  3. 边界(border)
  4. 子节点(children)
  5. 轮廓线(outline)
2.7.3 火狐显示列表(Firefox display list)

Firefox遍历渲染树并为绘制的矩形构建一个显示列表。它包含与矩形相关的渲染器(renderer),以正确的绘制顺序排列(渲染器的背景,然后是边框等)。

这样的话,只需要遍历一次树就可以重新绘制,而不是多次遍历,绘制所有的背景,然后是所有的图像,然后是所有的边框等等。

Firefox通过不添加将被隐藏的元素(比如完全隐藏在其他不透明元素之下的元素)来优化这个过程。

2.7.4 Webkit 矩形存储(Webkit rectangle storage)

在重新绘制之前,webkit 将旧矩形保存为位图。然后它只绘制新矩形和旧矩形之间的差异(delta)。

2.8 动态改变(Dynamic changes)

浏览器尝试执行尽可能少的操作来响应更改。因此,对元素颜色的更改只会导致元素的重新绘制。对元素位置的更改将导致元素及其子元素和可能的兄弟元素的布局和重新绘制。添加一个DOM节点将导致节点的布局和重新绘制。主要的变化,比如增加“html”元素的字体大小,将导致缓存失效,使得重新布局和绘制整个树。

2.9 渲染引擎的线程 (The rendering engine’s threads)

渲染引擎是单线程的。除了网络操作之外,几乎所有操作都在一个线程中进行。在Firefox和safari中,这是浏览器的主线程。在chrome中,它是标签进程的主线程。网络操作可以由多个并行线程执行。并行连接的数量是有限的(通常是2 - 6个连接。例如,Firefox 3 使用6 个连接)。

2.9.1 事件循环(Event loop)

浏览器主线程是一个事件循环。它是一个无限循环,保持进程的活性。它等待事件(如布局和绘制事件)并处理它们。这是主事件循环的Firefox代码:

while (!mExiting)
    NS_ProcessNextEvent(thread);

2.10 CSS2 可视化模型(CSS2 visual model)

2.10.1 canvas

根据CCS2规范,术语canvas描述了“呈现格式化结构的空间”,在里面浏览器绘制其内容。

canvas对于空间的每个尺寸都是无限的,但是浏览器根据viewport的尺寸选择初始宽度。

根据http://www.w3.org/TR/CSS2/zindex.html,如果一个canvas包含在另一个画布中,那么它就是透明的;如果不包含,则给出一个浏览器定义的颜色。

2.10.2 CSS Box model

CSS盒模型(Box model)描述为文档树中的元素生成的矩形框,并根据可视化格式模型进行布局。

每个box都有一个内容区域(例如,文本、图像等)和可选的周围填充、边框和边距区域。

图14.
在这里插入图片描述

每个节点生成0…n这样的盒子。

所有元素都有一个“display”属性,用于确定将生成的盒的类型。

例子:

block  - generates a block box.
inline - generates one or more inline boxes.
none - no box is generated.

默认值是内联的,但是浏览器样式表设置了其他默认值。例如,“div”元素的默认显示是块(block)。
你可以在 http://www.w3.org/TR/CSS2/sample.html 中找到一个默认样式表示例

2.10.3 定位方案(Positioning scheme)

这里有三种方案:

  1. Normal : 对象是根据其在文档中的位置来定位的。 这意味着它在渲染树中的位置类似于它在dom树中的位置,并根据它的盒类型和尺寸进行布局。
  2. Float: 首先将对象像普通流一样布局,然后尽可能向左或向右移动。
  3. Absolute: 对象在渲染树中的位置与它在DOM树中的位置不同。

定位方案由“position”属性和“float”属性设置。

  • static 和 relative 导致正常流
  • absolute 和 fixed 导致绝对定位

在static定位中,不定义位置,使用默认的定位。在其他方案中,开发者指定了位置——顶部、底部、左侧、右侧。
box的布局方式由以下因素决定:

  • 盒类型(Box type)
  • 盒尺寸(Box dimensions)
  • 定位方案(Position Scheme)
  • 外部的信息(External information) ,像图像大小和屏幕尺寸
2.10.4 盒类型(Box types)

Block box: 块形式,在浏览器窗口上有自己的矩形。

图15. Block box

在这里插入图片描述

Inline box: 没有自己的块,而是在一个包含块(containing block)内。

图15. Inline boxes
在这里插入图片描述

块被一个接一个地垂直排列形式。内联的格式是水平的。

图16. Block 和 Inline 形式

在这里插入图片描述

内联盒(Inline boxes)放在行(lines)或“行框(line boxes)”中。这些线条(lines)至少和最高的盒子一样高,但当盒子对齐“基线(baseline)”时,线条可以更高,“基线”是指元素的底部与另一个盒子的另一点对齐,而不是底部。如果容器宽度不够,内联将被放在几行。这通常发生在段落中。

图17. Lines
在这里插入图片描述

2.10.5 定位(Positioning)

1) Relative

Relative positioning , 像往常一样定位,然后移动所需差距(delta, Δ \Delta Δ)。

图18.
在这里插入图片描述

2) Floats

一个浮动框被移动到一行的左边或右边。有趣的功能是,其他文档会将其围绕,比如:

<p>
    <img style="float:right" src="images/image.gif" width="100" height="100">Lorem ipsum dolor sit amet, consectetuer...
</p>

将会看到如下效果:

图19.
在这里插入图片描述

3) Absolute 和 Fixed

无论正常的流程如何,布局都被精确地定义。元素不参与正常的流。尺寸与容器有关。在fixed属性下,容器是视图端口。

图20.
在这里插入图片描述

注意:即使文档被滚动,固定框(fixed box)也不会移动!

2.10.6 分层表示(Layered representation)

它由 z-index CSS属性指定。它代表盒子的第三维,它在“z轴”上的位置。

盒子被分成栈(称为栈上下文)。在每个栈中,后面的元素将首先被绘制,前面的元素将被绘制在顶部,离用户更近。在重叠的情况下,将隐藏前一个元素。

栈是根据z-index属性排序的。带有“z-index”属性的box形成一个本地栈。viewport有外部栈。

比如:

<STYLE type="text/css">
      div { 
        position: absolute; 
        left: 2in; 
        top: 2in; 
      }
</STYLE>
<P>   
    <DIV 
         style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
    </DIV>
    <DIV
         style="z-index: 1;background-color:green;width: 2in; height: 2in;">
    </DIV>
</p>

效果如下:

图20.
在这里插入图片描述

虽然绿色的div出现在红色的div之前,而且应该在常规流中绘制,但是z-index属性更高,所以它在root stack 所持有的栈中更靠前。

2.11 资源(Resources)

  1. Browser architecture
    1. Grosskurth, Alan. A Reference Architecture for Web Browsers. http://grosskurth.ca/papers/browser-refarch.pdf.
  2. Parsing
    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the “Dragon book”), Addison-Wesley, 1986

    2. Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5. http://broadcast.oreilly.com/2009/05/the-bold-and-the-beautiful-two.html.

  3. Firefox
    1. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers. http://dbaron.org/talks/2008-11-12-faster-html-and-css/slide-6.xhtml.
    2. L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers(Google tech talk video). http://www.youtube.com/watch?v=a2_6bGNZ7bA.
    3. L. David Baron, Mozilla’s Layout Engine. http://www.mozilla.org/newlayout/doc/layout-2006-07-12/slide-6.xhtml.
    4. L. David Baron, Mozilla Style System Documentation. http://www.mozilla.org/newlayout/doc/style-system.html.
    5. Chris Waterson, Notes on HTML Reflow. http://www.mozilla.org/newlayout/doc/reflow.html.
    6. Chris Waterson, Gecko Overview. http://www.mozilla.org/newlayout/doc/gecko-overview.htm.
    7. Alexander Larsson, The life of an HTML HTTP request. https://developer.mozilla.org/en/The_life_of_an_HTML_HTTP_request.
  4. Webkit
    1. David Hyatt, Implementing CSS(part 1). http://weblogs.mozillazine.org/hyatt/archives/cat_safari.html.
    2. David Hyatt, An Overview of WebCore. http://weblogs.mozillazine.org/hyatt/WebCore/chapter2.html.
    3. David Hyatt, WebCore Rendering. http://webkit.org/blog/114/.
    4. David Hyatt, The FOUC Problem. http://webkit.org/blog/66/the-fouc-problem/.
  5. W3C Specifications
    1. HTML 4.01 Specification. http://www.w3.org/TR/html4/.
    2. HTML5 Specification. http://dev.w3.org/html5/spec/Overview.html.
    3. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. http://www.w3.org/TR/CSS2/.
  6. Browsers build instructions
    1. Firefox. https://developer.mozilla.org/en/Build_Documentation
    2. Webkit. http://webkit.org/building/build.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值