文章篇幅很长,但讲得很详细很底层,还有示例讲解。感谢做者和译者。原文:http://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#Sharing_style_datacss
这是一篇全面介绍 Webkit 和 Gecko 内部操做的入门文章,是以色列开发人员塔利·加希尔大量研究的成果。在过去的几年中,她查阅了全部公开发布的关于浏览器内部机制的数据(请参见资源),并花了不少时间来研读网络浏览器的源代码。她写道:html
在 IE 占据 90% 市场份额的年代,咱们除了把浏览器当成一个“黑箱”,什么也作不了。可是如今,开放源代码的浏览器拥有了 过半的市场份额,所以,是时候来揭开神秘的面纱,一探网络浏览器的内幕了。呃,里面只有数以百万行计的 C++ 代码...塔利在 她的网站上公布了本身的研究成果,可是咱们以为它值得让更多的人来了解,因此咱们在此从新整理并公布。
做为一名网络开发人员,学习浏览器的内部工做原理将有助于您做出更明智的决策,并理解那些最佳开发实践的个中原因。尽管这是一篇至关长的文档,可是咱们建议您花些时间来仔细阅读;读完以后,您确定会以为所费不虚。保罗·爱丽诗 (Paul Irish),Chrome 浏览器开发人员事务部html5
网络浏览器极可能是使用最广的软件。在这篇入门文章中,我将会介绍它们的幕后工做原理。咱们会了解到,从您在地址栏输入 google.com
直到您在浏览器屏幕上看到 Google 首页的整个过程当中都发生了些什么。node
目前使用的主流浏览器有五个:Internet Explorer、Firefox、Safari、Chrome 浏览器和 Opera。本文中以开放源代码浏览器为例,即 Firefox、Chrome 浏览器和 Safari(部分开源)。根据 StatCounter 浏览器统计数据,目前(2011 年 8 月)Firefox、Safari 和 Chrome 浏览器的总市场占有率将近 60%。因而可知,现在开放源代码浏览器在浏览器市场中占据了很是坚实的部分。web
浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展现您选择的网络资源。这里所说的资源通常是指 HTML 文档,也能够是 PDF、图片或其余的类型。资源的位置由用户使用 URI(统一资源标示符)指定。正则表达式
浏览器解释并显示 HTML 文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。
多年以来,各浏览器都没有彻底听从这些规范,同时还在开发本身独有的扩展程序,这给网络开发人员带来了严重的兼容性问题。现在,大多数的浏览器都是或多或少地听从规范。算法
浏览器的用户界面有不少彼此相同的元素,其中包括:数据库
奇怪的是,浏览器的用户界面并无任何正式的规范,这是多年来的最佳实践天然发展以及彼此之间相互模仿的结果。HTML5 也没有定义浏览器必须具备的用户界面元素,但列出了一些通用的元素,例如地址栏、状态栏和工具栏等。固然,各浏览器也能够有本身独特的功能,好比 Firefox 的下载管理器。express
浏览器的主要组件为 (1.1):canvas
值得注意的是,和大多数浏览器不一样,Chrome 浏览器的每一个标签页都分别对应一个呈现引擎实例。每一个标签页都是一个独立的进程。
呈现引擎的做用嘛...固然就是“呈现”了,也就是在浏览器的屏幕上显示请求的内容。
默认状况下,呈现引擎可显示 HTML 和 XML 文档与图片。经过插件(或浏览器扩展程序),还能够显示其它类型的内容;例如,使用 PDF 查看器插件就能显示 PDF 文档。可是在本章中,咱们将集中介绍其主要用途:显示使用 CSS 格式化的 HTML 内容和图片。
本文所讨论的浏览器(Firefox、Chrome 浏览器和 Safari)是基于两种呈现引擎构建的。Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的呈现引擎。而 Safari 和 Chrome 浏览器使用的都是 Webkit。
Webkit 是一种开放源代码呈现引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows。有关详情,请参阅 webkit.org。
呈现引擎一开始会从网络层获取请求文档的内容,内容的大小通常限制在 8000 个块之内。
而后进行以下所示的基本流程:
呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于建立另外一个树结构:呈现树。
呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。
呈现树构建完毕以后,进入“布局”处理阶段,也就是为每一个节点分配一个应出如今屏幕上的确切坐标。下一个阶段是绘制 - 呈现引擎会遍历呈现树,由用户界面后端层将每一个节点绘制出来。
须要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它没必要等到整个 HTML 文档解析完毕以后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其他内容的同时,呈现引擎会将部份内容解析并显示出来。
从图 3 和图 4 能够看出,虽然 Webkit 和 Gecko 使用的术语略有不一样,但总体流程是基本相同的。
Gecko 将视觉格式化元素组成的树称为“框架树”。每一个元素都是一个框架。Webkit 使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,Webkit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于链接 DOM 节点和可视化信息从而建立呈现树的过程,Webkit 使用的术语是“附加”。有一个细微的非语义差异,就是 Gecko 在 HTML 与 DOM 树之间还有一个称为“内容槽”的层,用于生成 DOM 元素。咱们会逐一论述流程中的每一部分:
解析是呈现引擎中很是重要的一个环节,所以咱们要更深刻地讲解。首先,来介绍一下解析。
解析文档是指将文档转化成为有意义的结构,也就是可以让代码理解和使用的结构。解析获得的结果一般是表明了文档结构的节点树,它称做解析树或者语法树。
示例 - 解析 2 + 3 - 1 这个表达式,会返回下面的树:
解析是以文档所遵循的语法规则(编写文档所用的语言或格式)为基础的。全部能够解析的格式都必须对应肯定的语法(由词汇和语法规则构成)。这称为与上下文无关的语法。人类语言并不属于这样的语言,所以没法用常规的解析技术进行解析。
解析的过程能够分红两个子过程:词法分析和语法分析。
词法分析是将输入内容分割成大量标记的过程。标记是语言中的词汇,即构成内容的单位。在人类语言中,它至关于语言字典中的单词。
语法分析是应用语言的语法规则的过程。
解析器一般将解析工做分给如下两个组件来处理:词法分析器(有时也称为标记生成器),负责将输入内容分解成一个个有效标记;而解析器负责根据语言的语法规则分析文档的结构,从而构建解析树。词法分析器知道如何将无关的字符(好比空格和换行符)分离出来。
解析是一个迭代的过程。一般,解析器会向词法分析器请求一个新标记,并尝试将其与某条语法规则进行匹配。若是发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,而后继续请求下一个标记。
若是没有规则能够匹配,解析器就会将标记存储到内部,并继续请求标记,直至找到可与全部内部存储的标记匹配的规则。若是找不到任何匹配规则,解析器就会引起一个异常。这意味着文档无效,包含语法错误。
不少时候,解析树还不是最终产品。解析一般是在翻译过程当中使用的,而翻译是指将输入文档转换成另外一种格式。编译就是这样一个例子。编译器可将源代码编译成机器代码,具体过程是首先将源代码解析成解析树,而后将解析树翻译成机器代码文档。
在图 5 中,咱们经过一个数学表达式创建了解析树。如今,让咱们试着定义一个简单的数学语言,用来演示解析的过程。
词汇:咱们用的语言可包含整数、加号和减号。
语法:
让咱们分析一下 2 + 3 - 1。
匹配语法规则的第一个子串是 2,而根据第 5 条语法规则,这是一个项。匹配语法规则的第二个子串是 2 + 3,而根据第 3 条规则(一个项接一个运算符,而后再接一个项),这是一个表达式。下一个匹配项已经到了输入的结束。2 + 3 - 1 是一个表达式,由于咱们已经知道 2 + 3 是一个项,这样就符合“一个项接一个运算符,而后再接一个项”的规则。2 + + 不与任何规则匹配,所以是无效的输入。
词汇一般用正则表达式表示。
例如,咱们的示例语言能够定义以下:
INTEGER :0|[1-9][0-9]* PLUS :+ MINUS:-正如您所看到的,这里用正则表达式给出了整数的定义。
语法一般使用一种称为 BNF 的格式来定义。咱们的示例语言能够定义以下:
expression := term operation term operation := PLUS | MINUS term := INTEGER | expression
以前咱们说过,若是语言的语法是与上下文无关的语法,就能够由常规解析器进行解析。与上下文无关的语法的直观定义就是能够彻底用 BNF 格式表达的语法。有关正式定义,请参阅关于与上下文无关的语法的维基百科文章。
有两种基本类型的解析器:自上而下解析器和自下而上解析器。直观地来讲,自上而下的解析器从语法的高层结构出发,尝试从中找到匹配的结构。而自下而上的解析器从低层规则出发,将输入内容逐步转化为语法规则,直至知足高层规则。
让咱们来看看这两种解析器会如何解析咱们的示例:
自上而下的解析器会从高层的规则开始:首先将 2 + 3 标识为一个表达式,而后将 2 + 3 - 1 标识为一个表达式(标识表达式的过程涉及到匹配其余规则,可是起点是最高级别的规则)。
自下而上的解析器将扫描输入内容,找到匹配的规则后,将匹配的输入内容替换成规则。如此继续替换,直到输入内容的结尾。部分匹配的表达式保存在解析器的堆栈中。
堆栈 | 输入 |
---|---|
2 + 3 - 1 | |
项 | + 3 - 1 |
项运算 | 3 - 1 |
表达式 | - 1 |
表达式运算符 | 1 |
表达式 |
有一些工具能够帮助您生成解析器,它们称为解析器生成器。您只要向其提供您所用语言的语法(词汇和语法规则),它就会生成相应的解析器。建立解析器须要对解析有深入理解,而人工建立优化的解析器并非一件容易的事情,因此解析器生成器是很是实用的。
Webkit 使用了两种很是有名的解析器生成器:用于建立词法分析器的 Flex 以及用于建立解析器的 Bison(您也可能遇到 Lex 和 Yacc 这样的别名)。Flex 的输入是包含标记的正则表达式定义的文件。Bison 的输入是采用 BNF 格式的语言语法规则。
HTML 解析器的任务是将 HTML 标记解析成解析树。
HTML 的词汇和语法在 W3C 组织建立的规范中进行了定义。当前的版本是 HTML4,HTML5 正在处理过程当中。
正如咱们在解析过程的简介中已经了解到的,语法能够用 BNF 等格式进行正式定义。
很遗憾,全部的常规解析器都不适用于 HTML(我并非开玩笑,它们能够用于解析 CSS 和 JavaScript)。HTML 并不能很容易地用解析器所需的与上下文无关的语法来定义。
有一种能够定义 HTML 的正规格式:DTD(Document Type Definition,文档类型定义),但它不是与上下文无关的语法。
这初看起来很奇怪:HTML 和 XML 很是类似。有不少 XML 解析器可使用。HTML 存在一个 XML 变体 (XHTML),那么有什么大的区别呢?
区别在于 HTML 的处理更为“宽容”,它容许您省略某些隐式添加的标记,有时还能省略一些起始或者结束标记等等。和 XML 严格的语法不一样,HTML 总体来看是一种“软性”的语法。
显然,这种看上去细微的差异实际上却带来了巨大的影响。一方面,这是 HTML 如此流行的缘由:它能包容您的错误,简化网络开发。另外一方面,这使得它很难编写正式的语法。归纳地说,HTML 没法很容易地经过常规解析器解析(由于它的语法不是与上下文无关的语法),也没法经过 XML 解析器来解析。
HTML 的定义采用了 DTD 格式。此格式可用于定义 SGML 族的语言。它包括全部容许使用的元素及其属性和层次结构的定义。如上文所述,HTML DTD 没法构成与上下文无关的语法。
DTD 存在一些变体。严格模式彻底遵照 HTML 规范,而其余模式可支持之前的浏览器所使用的标记。这样作的目的是确保向下兼容一些早期版本的内容。最新的严格模式 DTD 能够在这里找到:www.w3.org/TR/html4/strict.dtd
解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。
解析树的根节点是“Document”对象。
DOM 与标记之间几乎是一一对应的关系。好比下面这段标记:
<html><body><p> Hello World </p><div><imgsrc="example.png"/></div></body></html>可翻译成以下的 DOM 树:
和 HTML 同样,DOM 也是由 W3C 组织指定的。请参见www.w3.org/DOM/DOMTR。这是关于文档操做的通用规范。其中一个特定模块描述针对 HTML 的元素。HTML 的定义能够在这里找到:www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html。
我所说的树包含 DOM 节点,指的是树是由实现了某个 DOM 接口的元素构成的。浏览器所用的具体实现也会具备一些其余属性,供浏览器在内部使用。
咱们在以前章节已经说过,HTML 没法用常规的自上而下或自下而上的解析器进行解析。
缘由在于:
document.write
,就会添加额外的标记,这样解析过程实际上就更改了输入内容。因为不能使用常规的解析技术,浏览器就建立了自定义的解析器来解析 HTML。
HTML5 规范详细地描述了解析算法。此算法由两个阶段组成:标记化和树构建。
标记化是词法分析过程,将输入内容解析成多个标记。HTML 标记包括起始标记、结束标记、属性名称和属性值。
标记生成器识别标记,传递给树构造器,而后接受下一个字符以识别下一个标记;如此反复直到输入的结束。
该算法的输出结果是 HTML 标记。该算法使用状态机来表示。每个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即便接收的字符相同,对于下一个正确的状态也会产生不一样的结果,具体取决于当前的状态。该算法至关复杂,没法在此详述,因此咱们经过一个简单的示例来帮助你们理解其原理。
基本示例 - 将下面的 HTML 代码标记化:
<html><body> Hello world </body></html>
初始状态是数据状态。遇到字符 <
时,状态更改成“标记打开状态”。接收一个 a-z
字符会建立“起始标记”,状态更改成“标记名称状态”。这个状态会一直保持到接收>
字符。在此期间接收的每一个字符都会附加到新的标记名称上。在本例中,咱们建立的标记是 html
标记。
遇到 >
标记时,会发送当前的标记,状态改回“数据状态”。<body>
标记也会进行一样的处理。目前 html
和 body
标记均已发出。如今咱们回到“数据状态”。接收到 Hello world
中的 H
字符时,将建立并发送字符标记,直到接收 </body>
中的<
。咱们将为 Hello world
中的每一个字符都发送一个字符标记。
如今咱们回到“标记打开状态”。接收下一个输入字符 /
时,会建立 end tag token
并改成“标记名称状态”。咱们会再次保持这个状态,直到接收 >
。而后将发送新的标记,并回到“数据状态”。</html>
输入也会进行一样的处理。
在建立解析器的同时,也会建立 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各类元素。标记生成器发送的每一个节点都会由树构建器进行处理。规范中定义了每一个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时建立。这些元素不只会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也能够用状态机来描述。这些状态称为“插入模式”。
让咱们来看看示例输入的树构建过程:
<html><body> Hello world </body></html>
树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是“initial mode”。接收 HTML 标记后转为“before html”模式,并在这个模式下从新处理此标记。这样会建立一个 HTMLHtmlElement 元素,并将其附加到 Document 根对象上。
而后状态将改成“before head”。此时咱们接收“body”标记。即便咱们的示例中没有“head”标记,系统也会隐式建立一个 HTMLHeadElement,并将其添加到树中。
如今咱们进入了“in head”模式,而后转入“after head”模式。系统对 body 标记进行从新处理,建立并插入 HTMLBodyElement,同时模式转变为“body”。
如今,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会建立并插入“Text”节点,而其余字符也将附加到该节点。
接收 body 结束标记会触发“after body”模式。如今咱们将接收 HTML 结束标记,而后进入“after after body”模式。接收到文件结束标记后,解析过程就此结束。
在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于“deferred”模式的脚本,也就是那些应在文档解析完成后才执行的脚本。而后,文档状态将设置为“完成”,一个“加载”事件将随之触发。
您在浏览 HTML 网页时历来不会看到“语法无效”的错误。这是由于浏览器会纠正任何无效内容,而后继续工做。
如下面的 HTML 代码为例:
<html><mytag></mytag><div><p></div> Really lousy HTML </p></html>
在这里,我已经违反了不少语法规则(“mytag”不是标准的标记,“p”和“div”元素之间的嵌套有误等等),可是浏览器仍然会正确地显示这些内容,而且毫无怨言。由于有大量的解析器代码会纠正 HTML 网页做者的错误。
不一样浏览器的错误处理机制至关一致,但使人称奇的是,这种机制并非 HTML 当前规范的一部分。和书签管理以及前进/后退按钮同样,它也是浏览器在多年发展中的产物。不少网站都广泛存在着一些已知的无效 HTML 结构,每一种浏览器都会尝试经过和其余浏览器同样的方式来修复这些无效结构。
HTML5 规范定义了一部分这样的要求。Webkit 在 HTML 解析器类的开头注释中对此作了很好的归纳。
解析器对标记化输入内容进行解析,以构建文档树。若是文档的格式正确,就直接进行解析。
遗憾的是,咱们不得不处理不少格式错误的 HTML 文档,因此解析器必须具有必定的容错性。
咱们至少要可以处理如下错误状况:
- 明显不能在某些外部标记中添加的元素。在此状况下,咱们应该关闭全部标记,直到出现禁止添加的元素,而后再加入该元素。
- 咱们不能直接添加的元素。这极可能是网页做者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
- 向 inline 元素内添加 block 元素。关闭全部 inline 元素,直到出现下一个较高级的 block 元素。
- 若是这样仍然无效,可关闭全部元素,直到能够添加元素为止,或者忽略该标记。
让咱们看一些 Webkit 容错的示例:
有些网站使用了 </br> 而不是 <br>。为了与 IE 和 Firefox 兼容,Webkit 将其与 <br> 作一样的处理。
代码以下:
if(t->isCloseTag(brTag)&& m_document->inCompatMode()){ reportError(MalformedBRError); t->beginTag =true;}请注意,错误处理是在内部进行的,用户并不会看到这个过程。
离散表格是指位于其余表格内容中,但又不在任何一个单元格内的表格。
好比如下的示例:
<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 使用一个堆栈来保存当前的元素内容,它会从外部表格的堆栈中弹出内部表格。如今,这两个表格就变成了同级关系。
若是用户在一个表单元素中又放入了另外一个表单,那么第二个表单将被忽略。
代码以下:
if(!m_currentFormElement){ m_currentFormElement =newHTMLFormElement(formTag, m_document);}
代码的注释已经说得很清楚了。
boolHTMLParser::allowNestedRedundantTag(constAtomicString& tagName){unsigned i =0;for(HTMLStackElem* curr = m_blockStack; i < cMaxRedundantTagDepth && curr && curr->tagName == tagName; curr = curr->next, i++){}return i != cMaxRedundantTagDepth;}
一样,代码的注释已经说得很清楚了。
支持格式很是糟糕的 HTML 代码。咱们从不关闭 body 标记,由于一些愚蠢的网页会在实际文档结束以前就关闭。咱们经过调用 end() 来执行关闭操做。
if(t->tagName == htmlTag || t->tagName == bodyTag )return;因此网页做者须要注意,除非您想做为反面教材出如今 Webkit 容错代码段的示例中,不然还请编写格式正确的 HTML 代码。
还记得简介中解析的概念吗?和 HTML 不一样,CSS 是上下文无关的语法,可使用简介中描述的各类解析器进行解析。事实上,CSS 规范定义了 CSS 的词法和语法。
让咱们来看一些示例:
词法语法(词汇)是针对各个标记用正则表达式定义的:
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*]')'];解释:这是一个规则集的结构:
div.error , a.error {color:red;font-weight:bold;}div.error 和 a.error 是选择器。大括号内的部分包含了由此规则集应用的规则。此结构的正式定义是这样的:
ruleset: selector [',' S* selector ]*'{' S* declaration [';' S* declaration ]*'}' S*;这表示一个规则集就是一个选择器,或者由逗号和空格(S 表示空格)分隔的多个(数量可选)选择器。规则集包含了大括号,以及其中的一个或多个(数量可选)由分号分隔的声明。“声明”和“选择器”将由下面的 BNF 格式定义。
Webkit 使用 Flex 和 Bison 解析器生成器,经过 CSS 语法文件自动建立解析器。正如咱们以前在解析器简介中所说,Bison 会建立自下而上的移位归约解析器。Firefox 使用的是人工编写的自上而下的解析器。这两种解析器都会将 CSS 文件解析成 StyleSheet 对象,且每一个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其余与 CSS 语法对应的对象。
网络的模型是同步的。网页做者但愿解析器遇到 <script> 标记时当即解析并执行脚本。文档的解析将中止,直到脚本执行完毕。若是脚本是外部的,那么解析过程会中止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。做者也能够将脚本标注为“defer”,这样它就不会中止文档解析,而是等到解析结束才执行。HTML5 增长了一个选项,可将脚本标记为异步,以便由其余线程解析和执行。
Webkit 和 Firefox 都进行了这项优化。在执行脚本时,其余线程会解析文档的其他部分,找出并加载须要经过网络加载的其余资源。经过这种方式,资源能够在并行链接上加载,从而提升整体速度。请注意,预解析器不会修改 DOM 树,而是将这项工做交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
另外一方面,样式表有着不一样的模型。理论上来讲,应用样式表不会更改 DOM 树,所以彷佛没有必要等待样式表并中止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。若是当时尚未加载和解析样式,脚本就会得到错误的回复,这样显然会产生不少问题。这看上去是一个非典型案例,但事实上很是广泛。Firefox 在样式表加载和解析的过程当中,会禁止全部脚本。而对于 Webkit 而言,仅当脚本尝试访问的样式属性可能受还没有加载的样式表影响时,它才会禁止该脚本。
在 DOM 树构建的同时,浏览器还会构建另外一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的做用是让您按照正确的顺序绘制内容。
Firefox 将呈现树中的元素称为“框架”。Webkit 使用的术语是呈现器或呈现对象。
呈现器知道如何布局并将自身及其子元素绘制出来。
Webkits RenderObject 类是全部呈现器的基类,其定义以下:
classRenderObject{virtualvoid layout();virtualvoid paint(PaintInfo);virtualvoid rect repaintRect();Node* node;//the DOM nodeRenderStyle* style;// the computed styleRenderLayer* containgLayer;//the containing z-index layer}
每个呈现器都表明了一个矩形的区域,一般对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。
框的类型会受到与节点相关的“display”样式属性的影响(请参阅样式计算章节)。下面这段 Webkit 代码描述了根据 display 属性的不一样,针对同一个 DOM 节点应建立什么类型的呈现器。
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;}元素类型也是考虑因素之一,例如表单控件和表格都对应特殊的框架。
createRenderer
方法。呈现器所指向的样式对象中包含了一些和几何无关的信息。
有一些 DOM 元素对应多个可视化对象。它们每每是具备复杂结构的元素,没法用单一的矩形来描述。例如,“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。若是因为宽度不够,文本没法在一行中显示而分为多行,那么新的行也会做为新的呈现器而添加。
另外一个关于多呈现器的例子是格式无效的 HTML。根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。若是出现了混合内容,则应建立匿名的 block 呈现器,以包裹 inline 元素。
有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不一样。浮动定位和绝对定位的元素就是这样,它们处于正常的流程以外,放置在树中的其余地方,并映射到真正的框架,而放在原位的是占位框架。
在 Firefox 中,系统会针对 DOM 更新注册展现层,做为侦听器。展现层将框架建立工做委托给 FrameConstructor
,由该构造器解析样式(请参阅样式计算)并建立框架。
在 Webkit 中,解析样式和建立呈现器的过程称为“附加”。每一个 DOM 节点都有一个“attach”方法。附加是同步进行的,将节点插入 DOM 树须要调用新的节点“attach”方法。
处理 html 和 body 标记就会构建呈现树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block,这是最上层的 block,包含了其余全部 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame
,而 Webkit 称之为 RenderView
。这就是文档所指向的呈现对象。呈现树的其他部分以 DOM 树节点插入的形式来构建。
请参阅关于处理模型的 CSS2 规范。
构建呈现树时,须要计算每个呈现对象的可视化属性。这是经过计算每一个元素的样式属性来完成的。
样式包括来自各类来源的样式表、inline 样式元素和 HTML 中的可视化属性(例如“bgcolor”属性)。其中后者将通过转化以匹配 CSS 样式属性。
样式表的来源包括浏览器的默认样式表、由网页做者提供的样式表以及由浏览器用户提供的用户样式表(浏览器容许您定义本身喜欢的样式。以 Firefox 为例,用户能够将本身喜欢的样式表放在“Firefox Profile”文件夹下)。
样式计算存在如下难点:
若是不进行优化,为每个元素查找匹配的规则会形成性能问题。要为每个元素遍历整个规则列表来寻找匹配规则,这是一项浩大的工程。选择器会具备很复杂的结构,这就会致使某个匹配过程一开始看起来极可能是正确的,但最终发现实际上是徒劳的,必须尝试其余匹配路径。
例以下面这个组合选择器:
div div div div{...}这意味着规则适用于做为 3 个 div 元素的子代的
<div>
。若是您要检查规则是否适用于某个指定的 <div>
元素,应选择树上的一条向上路径进行检查。您可能须要向上遍历节点树,结果发现只有两个 div,并且规则并不适用。而后,您必须尝试树中的其余路径。Webkit 节点会引用样式对象 (RenderStyle)。这些对象在某些状况下能够由不一样节点共享。这些节点是同级关系,而且:
为了简化样式计算,Firefox 还采用了另外两种树:规则树和样式上下文树。Webkit 也有样式对象,但它们不是保存在相似样式上下文树这样的树结构中,只是由 DOM 节点指向此类对象的相关样式。
样式上下文包含端值。要计算出这些值,应按照正确顺序应用全部的匹配规则,并将其从逻辑值转化为具体的值。例如,若是逻辑值是屏幕大小的百分比,则须要换算成绝对的单位。规则树的点子真的很巧妙,它使得节点之间能够共享这些值,以免重复计算,还能够节约空间。
全部匹配的规则都存储在树中。路径中的底层节点拥有较高的优先级。规则树包含了全部已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为全部的节点进行计算,而是只有当某个节点样式须要进行计算时,才会向规则树添加计算的路径。
这个想法至关于将规则树路径视为词典中的单词。若是咱们已经计算出以下的规则树:
让咱们看看规则树如何帮助咱们减小工做。
样式上下文可分割成多个结构。这些结构体包含了特定类别(如 border 或 color)的样式信息。结构中的属性都是继承的或非继承的。继承属性若是未由元素定义,则继承自其父代。非继承属性(也称为“重置”属性)若是未进行定义,则使用默认值。
规则树经过缓存整个结构(包含计算出的端值)为咱们提供帮助。这一想法假定底层节点没有提供结构的定义,则可以使用上层节点中的缓存结构。
在计算某个特定元素的样式上下文时,咱们首先计算规则树中的对应路径,或者使用现有的路径。而后咱们沿此路径应用规则,在新的样式上下文中填充结构。咱们从路径中拥有最高优先级的底层节点(一般也是最特殊的选择器)开始,并向上遍历规则树,直到结构填充完毕。若是该规则节点对于此结构没有任何规范,那么咱们能够实现更好的优化:寻找路径更上层的节点,找到后指定完整的规范并指向相关节点便可。这是最好的优化方法,由于整个结构都能共享。这能够减小端值的计算量并节约内存。
若是咱们找到了部分定义,就会向上遍历规则树,直到结构填充完毕。
若是咱们找不到结构的任何定义,那么假如该结构是“继承”类型,咱们会在上下文树中指向父代的结构,这样也能够共享结构。若是是 reset 类型的结构,则会使用默认值。
若是最特殊的节点确实添加了值,那么咱们须要另外进行一些计算,以便将这些值转化成实际值。而后咱们将结果缓存在树节点中,供子代使用。
若是某个元素与其同级元素都指向同一个树节点,那么它们就能够共享整个样式上下文。
让咱们来看一个例子,假设咱们有以下 HTML 代码:
<html><body><divclass="err"id="div1"><p> this is a <spanclass="big"> big error </span> this is also a <spanclass="big"> very big error</span> error </p></div><divclass="err"id="div2">another error</div></body></html>还有以下规则:
为了简便起见,咱们只须要填充两个结构:color 结构和 margin 结构。color 结构只包含一个成员(即“color”),而 margin 结构包含四条边。
造成的规则树以下图所示(节点的标记方式为“节点名 : 指向的规则序号”):
假设咱们解析 HTML 时遇到了第二个 <div> 标记,咱们须要为此节点建立样式上下文,并填充其样式结构。
通过规则匹配,咱们发现该 <div> 的匹配规则是第 一、2 和 6 条。这意味着规则树中已有一条路径可供咱们的元素使用,咱们只须要再为其添加一个节点以匹配第 6 条规则(规则树中的 F 节点)。
咱们将建立样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的 F 节点。
如今咱们须要填充样式结构。首先要填充的是 margin 结构。因为最后的规则节点 (F) 并无添加到 margin 结构,咱们须要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,而后使用该结构。咱们会在指定 margin 规则的最上层节点(即 B 节点)上找到该结构。
咱们已经有了 color 结构的定义,所以不能使用缓存的结构。因为 color 有一个属性,咱们无需上溯规则树以填充其余属性。咱们将计算端值(将字符串转化为 RGB 等)并在此节点上缓存通过计算的结构。
第二个 <span> 元素处理起来更加简单。咱们将匹配规则,最终发现它和以前的 span 同样指向规则 G。因为咱们找到了指向同一节点的同级,就能够共享整个样式上下文了,只需指向以前 span 的上下文便可。
对于包含了继承自父代的规则的结构,缓存是在上下文树中进行的(事实上 color 属性是继承的,可是 Firefox 将其视为 reset 属性,并缓存到规则树上)。
例如,若是咱们在某个段落中添加 font 规则:
p {font-family:Verdana;font size:10px;font-weight:bold}那么,该段落元素做为上下文树中的 div 的子代,就会共享与其父代相同的 font 结构(前提是该段落没有指定 font 规则)。
在 Webkit 中没有规则树,所以会对匹配的声明遍历 4 次。首先应用非重要高优先级的属性(因为做为其余属性的依据而应首先应用的属性,例如 display),接着是高优先级重要规则,而后是普通优先级非重要规则,最后是普通优先级重要规则。这意味着屡次出现的属性会根据正确的层叠顺序进行解析。最后出现的最终生效。
所以归纳来讲,共享样式对象(整个对象或者对象中的部分结构)能够解决问题 1和问题 3。Firefox 规则树还有助于按照正确的顺序应用属性。
样式规则有一些来源:
p {color:blue}
<pstyle="color:blue"/>
<pbgcolor="blue"/>
后两种很容易和元素进行匹配,由于元素拥有样式属性,并且 HTML 属性可使用元素做为键值进行映射。
咱们以前在第 2 个问题中提到过,CSS 规则匹配可能比较棘手。为了解决这一难题,能够对 CSS 规则进行一些处理,以便访问。
样式表解析完毕后,系统会根据选择器将 CSS 规则添加到某个哈希表中。这些哈希表的选择器各不相同,包括 ID、类名称、标记名称等,还有一种通用哈希表,适合不属于上述类别的规则。若是选择器是 ID,规则就会添加到 ID 表中;若是选择器是类,规则就会添加到类表中,依此类推。
这种处理能够大大简化规则匹配。咱们无需查看每一条声明,只要从哈希表中提取元素的相关规则便可。这种优化方法可排除掉 95% 以上规则,所以在匹配过程当中根本就不用考虑这些规则了 (4.1)。
咱们以以下的样式规则为例:
p.error {color:red}#messageDiv {height:50px} div {margin:5px}第一条规则将插入类表,第二条将插入 ID 表,而第三条将插入标记表。
<pclass="error">an error occurred </p><divid=" messageDiv">this is a message</div>
咱们首先会为 p 元素寻找匹配的规则。类表中有一个“error”键,在下面能够找到“p.error”的规则。div 元素在 ID 表(键为 ID)和标记表中有相关的规则。剩下的工做就是找出哪些根据键提取的规则是真正匹配的了。
例如,若是 div 的对应规则以下:
table div {margin:5px}这条规则仍然会从标记表中提取出来,由于键是最右边的选择器,但这条规则并不匹配咱们的 div 元素,由于 div 没有 table 祖先。
Webkit 和 Firefox 都进行了这一处理。
样式对象具备每一个可视化属性一一对应的属性(均为 CSS 属性但更为通用)。若是某个属性未由任何匹配规则所定义,那么部分属性就可由父代元素样式对象继承。其余属性具备默认值。
若是定义不止一个,就会出现问题,须要经过层叠顺序来解决。
浏览器声明是重要程度最低的,而用户只有将该声明标记为“重要”才能够替换网页做者的声明。一样顺序的声明会根据特异性进行排序,而后再是其指定顺序。HTML 可视化属性会转换成匹配的 CSS 声明。它们被视为低优先级的网页做者规则。
选择器的特异性由 CSS2 规范定义以下:
您使用的进制取决于上述类别中的最高计数。
例如,若是 a=14,您可使用十六进制。若是 a=17,那么您须要使用十七进制;固然不太可能出现这种状况,除非是存在以下的选择器:html body 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 */
找到匹配的规则以后,应根据级联顺序将其排序。Webkit 对于较小的列表会使用冒泡排序,而对较大的列表则使用归并排序。对于如下规则,Webkit 经过替换“>”运算符来实现排序:
staticbooloperator>(CSSRuleData& r1,CSSRuleData& r2){int spec1 = r1.selector()->specificity();int spec2 = r2.selector()->specificity();return(spec1 == spec2): r1.position()> r2.position(): spec1 > spec2;}
Webkit 使用一个标记来表示是否全部的顶级样式表(包括 @imports)均已加载完毕。若是在附加过程当中还没有彻底加载样式,则使用占位符,并在文档中进行标注,等样式表加载完毕后再从新计算。
呈现器在建立完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。
HTML 采用基于流的布局模型,这意味着大多数状况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素一般不会影响靠前位置元素的几何特征,所以布局能够按从左至右、从上至下的顺序遍历文档。可是也有例外状况,好比 HTML 表格的计算就须要不止一次的遍历 (3.5)。
坐标系是相对于根框架而创建的,使用的是上坐标和左坐标。
布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 <html>
元素)开始,而后递归遍历部分或全部的框架层次结构,为每个须要计算的呈现器计算几何信息。
全部的呈现器都有一个“laybout”或者“reflow”方法,每个呈现器都会调用其须要进行布局的子代的 layout 方法。
为避免对全部细小更改都进行总体布局,浏览器采用了一种“dirty 位”系统。若是某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则须要进行布局。
有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代须要布局。
全局布局是指触发了整个呈现树范围的布局,触发缘由可能包括:
布局能够采用增量方式,也就是只对 dirty 呈现器进行布局(这样可能存在须要进行额外布局的弊端)。
当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树以后,新的呈现器附加到了呈现树中。
布局一般具备如下模式:
Firefox 使用“state”对象 (nsHTMLReflowState) 做为布局的参数(称为“reflow”),这其中包括了父呈现器的宽度。
Firefox 布局的输出为“metrics”对象 (nsHTMLReflowMetrics),其包含计算得出的呈现器高度。
呈现器宽度是根据容器块的宽度、呈现器样式中的“width”属性以及边距和边框计算得出的。
例如如下 div 的宽度:
<divstyle="width:30%"/>将由 Webkit 计算以下(BenderBox 类,calcWidth 方法):
clientWidth()- paddingLeft()- paddingRight()clientWidth 和 clientHeight 表示一个对象的内部(除去边框和滚动条)。
这些值会缓存起来,以用于须要布局而宽度不变的状况。
若是呈现器在布局过程当中须要换行,会当即中止布局,并告知其父代须要换行。父代会建立额外的呈现器,并对其调用布局。
在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工做是使用用户界面基础组件完成的。
Firefox 对此过程进行了优化,也就是不添加隐藏的元素,例如被不透明元素彻底遮挡住的元素。
while(!mExiting) NS_ProcessNextEvent(thread);
根据 CSS2 规范,“画布”这一术语是指“用来呈现格式化结构的空间”,也就是供浏览器绘制内容的区域。画布的空间尺寸大小是无限的,可是浏览器会根据视口的尺寸选择一个初始宽度。
根据 www.w3.org/TR/CSS2/zindex.html,画布若是包含在其余画布内,就是透明的;不然会由浏览器指定一种颜色。
CSS 框模型描述的是针对文档树中的元素而生成,并根据可视化格式模型进行布局的矩形框。
每一个框都有一个内容区域(例如文本、图片等),还有可选的周围补白、边框和边距区域。
每个节点都会生成 0..n 个这样的框。
全部元素都有一个“display”属性,决定了它们所对应生成的框类型。示例:
block - generates a block box.inline- generates one or more inline boxes. none -no box is generated.默认值是 inline,可是浏览器样式表设置了其余默认值。例如,“div”元素的 display 属性默认值是 block。
有三种定位方案:
定位方案是由“position”属性和“loat”属性设置的。
框的布局方式是由如下因素决定的:
block 框:造成一个 block,在浏览器窗口中拥有其本身的矩形区域。
inline 框:没有本身的 block,可是位于容器 block 内。
block 采用的是一个接一个的垂直格式,而 inline 采用的是水平格式。
inline 框放置在行中或“行框”中。这些行至少和最高的框同样高,还能够更高,当框根据“底线”对齐时,这意味着元素的底部须要根据其余框中非底部的位置对齐。若是容器的宽度不够,inline 元素就会分为多行放置。在段落中常常发生这种状况。
相对定位:先按照普通方式定位,而后根据所需偏移量进行移动。
浮动框会移动到行的左边或右边。有趣的特征在于,其余框会浮动在它的周围。下面这段 HTML 代码:
<p><imgstyle="float:right"src="images/image.gif"width="100"height="100"> Lorem ipsum dolor sit amet, consectetuer... </p>显示效果以下:
这种布局是准肯定义的,与普通流无关。元素不参与普通流。尺寸是相对于容器而言的。在固定定位中,容器就是可视区域。
这是由 z-index CSS 属性指定的。它表明了框的第三个维度,也就是沿“z 轴”方向的位置。
这些框分散到多个堆栈(称为堆栈上下文)中。在每个堆栈中,会首先绘制后面的元素,而后在顶部绘制前面的元素,以便更靠近用户。若是出现重叠,新绘制的元素就会覆盖以前的元素。
堆栈是按照 z-index 属性进行排序的。具备“z-index”属性的框造成了本地堆栈。视口具备外部堆栈。
示例:
<styletype="text/css"> div {position: absolute;left:2in;top:2in;}</style><p><divstyle="z-index:3;background-color:red;width:1in;height:1in;"></div><divstyle="z-index:1;background-color:green;width:2in;height:2in;"></div></p>结果以下:
虽然红色 div 在标记中的位置比绿色 div 靠前(按理应该在常规流程中优先绘制),可是 z-index 属性的优先级更高,所以它移动到了根框所保持的堆栈中更靠前的位置。
塔利·加希尔是以色列的一名开发人员。她在 2000 年开始从事网络开发工做,逐渐熟悉了 Netscape 的“邪恶”层模型。就像理查德·费曼 (Richard Feynmann) 同样,她极度热衷于探究事物的原理,所以开始深刻了解浏览器的内部原理,并记录研究成果。塔利还发表过一篇关于客户端性能的简短指南。