原文地址。css
( 译者注:这是一篇深度好文,而且附带官方简体中文。本次的翻译为一人完成,限于水平,不免有误,故须要学习本文内容的同窗请直接参考原文地址进行阅读。html
导读: 终于,我在一周以内讲这长篇大论的浏览器背后的故事翻译完了。若是再要我从新阅读一遍,可能须要我沐浴焚香般的准备。如今我忍着肩膀和手腕的酸痛,写下发布前的最后一些体会:html5
这是篇全面介绍 WebKit 和 Gecko 的内部操做的文章,它是以色列的开发者 Tail Garsiel 的大量的研究成果。过去几年,她从新审视了已公开的关于浏览器内部的资料(参考资料)同时花费了不少时间去阅读 web 浏览器源码。她写道:node
在 IE 90% 支配的那个年代,把浏览器当作一个“黑盒”再也合适不过了,可是如今,开源浏览器占据了一半的市场份额,是时候去了解引擎的背后同时看看 web 浏览器的内部。尽管,里面有数百万行 C++ 代码……c++
Tail 在她的网站上发布了她的研究,可是咱们想让更多的人知道,因此咱们已经从新整理再次发布在这里。web
做为一个 web 开发,了解浏览器操做的内部会帮助你作出更好的决定,同时在最佳开发实践时了解充分的理由。而这是一篇有长度的文章,咱们推荐你花点时间去深挖探究,咱们保证你会对本身的所作满意。 Paul lrish, Chrome 开发人员关系部正则表达式
这篇文章被翻译成几种语言:HTML5 Rocks 翻译了 德语,西班牙语,日语,葡萄牙语,俄语和简体中文版本。你也能够看到韩语和土耳其语。 你也能看看关于这篇主题 Tail Garsiel 在 Vimeo 上的谈话。算法
(译者注:这篇目录翻译了我半个小时,经过目录的回顾确实跟以前的一些零碎知识串联了起来,发现不少。更主要的是,跑个题来缓解下被这个目录吓尿的心脏。)express
Web 浏览器是使用最普遍的软件。这篇读物中,我会解释在场景以后他们是如何工做的。咱们将会看到,当你在地址栏输入 google.com 时直到在浏览器屏幕上看到 Google Page 页面后,发生了什么。canvas
现在经常使用的主要浏览器有 5 种: Chrome,IE,火狐,Safari 和 Opera。在移动端上,主要的浏览器是安卓浏览器,苹果,Opera 迷你和 Opera移动端还有 UC 浏览器,诺基亚 S40/S60 浏览器和 Chrome 也都是,除了 Opera 浏览器,其余都是基于 WebKit。(译者注:前一句话在官方简体中文里没有.)我从开源浏览器火狐和 Chrome 以及 Safari(部分开源)中距离。根据 StatCounter 统计(从2013年6月以来) Chrome 火狐和 Safari 组成了全球桌面浏览器使用量的 71%。在移动端,安卓浏览器,iPhone 和 Chrome 有 54% 的使用率。
浏览器的主要功能是展现你选择的 web 资源,经过服务端的请求而后在浏览器窗口展现。这个资源一般是一个 HTML 文档,但也有多是一个 PDF,一张图片,或者其余类型的内容。资源的位置经过用户使用 URI(Uniform Resource Identifier) 来明确指出。
浏览器插入和展现 HTML 文件的方式在 HTML 和 CSS 规范中有详细说明。这些规范经过 W3C(World Wide Web Consortium) 维护,这些规范也是 web 的标准组织。这些年的浏览器只是遵照一部分标准同时开发了他们本身的扩展。这对 web 开发者来讲引起了一系列的兼容性问题。现在大多数浏览器或多或少遵照这些规范。
浏览器用户界面相互有不少共同之处。它们之间的共同元素有:
奇怪的是,浏览器的用户界面没有任何形式的规范,它只是从过去几年的经验和经过浏览器相互模仿中产生的最佳实践。 HTML5 规范没有定义一个浏览器必须拥有的 UI 元素,可是列出了一些常见元素。它们有地址栏,状态栏和工具栏。这些内容,尤为是,像火狐的下载管理器对具体浏览器而言是特有的。
浏览器的主要组件有(1.1):
对于浏览器而言这是很重要的,好比 Chrome 运行多个渲染引擎的实例为每个标签。每一个标签有个独立的进程。
渲染引擎的责任是,额……渲染,也就是在浏览器屏幕上展现请求的内容。
默认的渲染引擎能够展现 HTML 和 XML 文档以及图片。它也能够经过插件或者扩展来展现其余的数据类型。举个例子,使用 PDF 视图插件展现 PDF 文档。然而,在本章节中咱们将关注它的主要用处:展现使用 CSS 格式化的 HTML 和 图片。
不一样的浏览器使用不一样的渲染引擎:IE 使用 Trident,火狐使用 Gecko,Safari 使用 WebKit。Chrome 和 Opera(自 15 版)使用 Blink,是Webkit 的一个分支。
WebKit是一个开源引擎,做为引擎,最开始在 Linux 平台上而后被 Apple 为了支持 Mac 和 Windows而修改。了解webkit.org的更多细节。
渲染引擎从网络层开始获取请求文档的内容。这个一般在一个 8KB 块中完成。
在那以后,展现了渲染引擎的基本流程:
渲染引擎开始解析 HTMl 文档同时在一个名叫“内容树”的树中转化元素变成 DOM 节点。引擎将会解析样式数据,外部的 CSS 文件和元素样式。在 HTML 中带有可视指令的样式信息将会被用于建立另外一个树:渲染树。
渲染树包含了有可视属性像是颜色和尺寸的矩形。这个矩形在屏幕上以正确的顺序展现。
以后的渲染树会经过一个“布局”进程。这意味着该进程会给在屏幕上应该出现的每一个节点一个精确的坐标。下一个阶段是绘制——渲染树被转换而后每一个节点经过 UI 后台层被绘制。
理解这个渐进过程是很是必要的。为了更好地用户体验,渲染引擎会尽快尝试在屏幕上展现内容。它在开始构建和布局渲染树以前,不会等待全部的 HTMl 被解析。一部份内容被解析和展现,而进程继续剩余的从网络来的内容。
从图 3 和图 4你能够看到,WebKit 和 Gecko 术语上有点不一样,过程仍是基本同样的。
Gecko 调用一个树,这个树是可视化的一个被格式化的“框架树”。每一个元素是一个框架。WebKit 使用的叫作“Render Tree”,同时它由“Render Objects”组成。WebKit 对元素位置使用“Layout”,而 Gecko 称它为 “Reflow”。“Attachment”是WebKit一个术语,用于链接 DOM 节点和可视化信息用于建立渲染树。一个不重要的非语义的不一样是 Gecko 在 HTML 和 DOM 树之间有一个额外的层,叫作 “content sink(内容沉淀)”,同时它是一个制做 DOM 元素的工场。咱们将会讨论过程的每一个部分:
由于在渲染引擎中,解析是很是明显的进程,咱们将会探索的深刻一点。经过关于解析的简单介绍来开始。
解析一个文档意味着翻译为代码可用的结构。解析的结果一般是一个节点树,这颗树表明着文档结构。一般叫作解析树或者语法树。
举个例子,解析表达式 2 + 3 -1
能够返回下面的树:
解析基于文件遵照的语法规则:语言或者写入的格式。全部能够解析的格式必须由词汇和句法规则构成肯定的语法。这称为上下文无关语法。人类语言不是这种语言,所以不能用常规的解析技术解析。
解析能够分为两个独立的子过程:词法分析和语法分析。
词法分析是将输入变成为标记的过程。标记是语言词汇:构建块的集合。在人类语言中它由全部在这个语言的字典中出现的单词构成。
语法分析是语言语法规则的应用。
解析一般在两个部分中独立的工做:词法分析器(有时也叫标记分析器),负责将输入变成有效地标记,同时解析器的责任是经过根据语言规则来分析文档构建解析树。词法分析器知道如何去除不相关的字符好比空格和换行。
解析过程是反复的。解析器一般为新的标记向词法分析器请求,并尝试将标记与某条语法规则匹配。若是规则匹配了,一个相应标记的节点将被添加到语法树中去,而且解析会请求下一个标记。
若是没有规则匹配,解析器会内部储存这个标记,而且保持请求标记直到一个匹配到全部储存在内部的标记的规则被发现。若是没有找到规则,那么解析器将抛出一个错误。这意味着文件无效而且包含语法错误。
在不少例子中,解析树不是最终产物。解析一般用于翻译:转换输入文档为另外一种格式。举个例子好比编译:编译器编译源码成为机器码,首先编译成编译树,而后再编译成机器码文件。
在图表5中,咱们从数学表达式中构建编译树。咱们试试定义一个简单的数学语言而后看看编译过程。
词汇:咱们的语言包括数字,加减号。 语法:
咱们来分析输入的: 2 + 3 - 1
首先匹配到的规则串是 2
:根据规则 #5 这是一个项。第二个匹配是 2 + 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
为一个表达式(识别表达式的过程是逐步的,匹配其余规则的,可是是从上层开始。)
自底向上解析将会浏览输入直到一个规则被匹配。它将用这个规则替换匹配。这会一直执行直到输入的末端。部分匹配到的表达式在解析栈上被替换。
Stack | Input |
---|---|
term | 2 + 3 -1 |
term operation | + 3 - 1 |
expression | 3 - 1 |
expression operation | - 1 |
expression | - |
这种自底向上的解析称为移入规约解析,由于输入偏移到右侧(想象一个指针在输入开始而后移动到右侧),而且逐渐减小语法规则。
有个工具能够生成解析。你告诉工具你的语言语法——它的词汇和语法规则——而后工具会生成一个有效解析。建立解析须要深入理解解析,而且手动建立一个优化的解析并不容易,因此解析生成器是颇有用的。
WebKit 使用两个著名的解析工具: Flex,用于建立词法,Bison,用于建立解析(你会或许把他们称为 Lex 和 Yacc)。Flex 输入是一个文件,包含标记的正则表达式定义。Bison 的输入是在 BNF 格式中的语言与法。
HTML 解析器的工做是把 HTML 标记转化成解析树。
HTML 的词汇和语法由 W3C 组织创造,在这个规范被定义。
如同咱们在解析中的介绍,语法能够像 BNF 那样使用格式化的格式被定义。
不幸的是,全部常规的解析方式不能应用于 HTML(为了好玩我不把它们如今引入——它们在解析 CSS 和 JavaScript时将被用到)。HTML 不能像解析器须要的那样经过上下文无关文法被定义。
乍一看这个表现很奇怪; HTML 十分像 XML。有很是多的 XML 解析器可使用。HTML 是 XML 的变体——因此有什么很大的不一样吗?
这里的不一样在于,HTML 尽量的更多“包容”:它能让你省略某些标签(那些被隐式添加的),或者有时候省略开始或结束标签等等。总体来看它是“软性语法,与 XML 的严格硬性语法不一样。
HTML 定义在一种 DTD 格式里。这种格式用于定义 SGML 家族的语言。这个格式为全部容许的元素,它们的属性和等级定义。咱们以前看到,HTML DTD 不是一种上下文无关文法。
DTD 有一些变体。严格模式适用于惟一的规范,可是其余模式包含对过去浏览器使用的标记的支持。这个目的是能够向后兼容老旧的内容。目前严格模式的 DTD 参考这里www.w3.org/TR/html4/st…。
输出树(解析树)是 DOM 元素和节点属性的树。DOM 是 Document Object Model 的缩写。它是 HTML 文档的表现对象和 HTML 元素对外部世界像是 JavaScript 元素的接口。树的根节点是 “Document” 对象。
DOM 对标记来讲几乎要一对一的关系。举个例子:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
复制代码
标记将会被转化为下面的 DOM 树:
像 HTML,DOM 经过 W3C 组织定义。参考这里www.w3.org/DOM/DOMTR。它是对操做文档的通常定义。特殊的模型描述了 HTML 特殊元素。HTML 定义能够在这里找到:www.w3.org/TR/2003/REC…。
当我谈到树包含 DOM 节点时,个人意思是这棵树是有结构的元素,实现了 DOM 其中之一的接口。浏览器混合了这些实现,这些实现有一些经过浏览器内部定义的其余属性。
如咱们以前看到的部分同样,HTML 不能使用常规的自顶向下或者自底向上解析。
缘由有:
document.write()
的脚本元素调用)能够添加额外的标记,因此解析过程实际上修改了输入。不能使用常规解析技术,浏览器为解析 HTML 建立了自定义解析。
由 HTML5 规范定义了解析算法的细节。算法有两个阶段组成:标记(断词)和结构树。
标记是词法分析,解析是输入变成标记。在 HTML 中,标记是开始标签,结束标签,属性名和属性值。
标记器识别标记,把标记给树构造器,而且为下个识别的标记处理下个字符,直到输入的结尾。
这个算法的输出是 HTML 标记。这个算法被做为状态机表达。每一个状态使用一个或者多个输入流的字符,而且根据这些字符更新下一个状态。这个决定经过当前标记状态和树构造状态影响。这就意味着消耗一样的字符为了正确的下个状态将会产出不一样的结果,这取决于当前状态。这个算法过于复杂,以至不能彻底描述,咱们来看看一个简单的例子,这能够帮助咱们理解这个规则。
基本例子:标记如下 HTML:
<html>
<body>
Hello world
</body>
</html>
复制代码
初始化状态是 “Data State”。当遇到 <
字符时,状态变成“Tag open state”。使用 a-z
的字符产生“Start tag token”的建立,状态变为“Tag name state”。咱们保留这个状态直到 >
字符出现。每一个字符都被添加到新的标记名称上。在咱们的例子中,这个建立的标记是 html
标记。
当 >
标签出现,当前标记被发送,同时状态变回 Data state
。<body>
标签也是用相同的步骤处理。目前为止,html
和 body
标签被发送了。咱们如今回到了 “Data state”。遇到 Hello world
字符的 H
将会引发建立和字符标记的发送,这将一直进行直到碰见 </body>
的 <
。咱们将为 Hello world
的每个字符发送一个字符标记。
如今咱们回到“Tag open state”。遇到下一个输入 /
将会引发结束标签的建立,而且移动到“Tag name state”。再一次咱们保持在这个状态,直到咱们碰见 >
。此时这个新的标签标记将被发送,而且咱们回到“Data state”。</html>
输入将像以前的例子同样被处理。
当建立文档对象的解析器被建立。在树构造阶段期间,以 Document 为根节点的 DOM 树也被修改,而且元素被添加进去。经过标记生成器发送的每一个节点将被树构造器处理。对于每一个标记,规范定义了 DOM 元素与之相关,同时有一个开放元素的栈。这个栈用于正确嵌套没有匹配和没有闭合的标签。这个算法也是做为一个状态机描述。这个状态叫作“insertion modes”(插入模式)。
咱们看看树构造过程输入的例子:
<html>
<body>
Hello world
</body>
</html>
复制代码
从标记阶段中,输入给树构造器的阶段是连续的标记。初始模式是 “initial mode”。接收到 “html” 标记将会移动到 “before html” 模式,而且在那个模式中再处理标记。这将引起 HTMLHtmlElement 元素建立,它将被添加到 Document 对象的根节点中。
状态将被变为 “before head”。“body” 标记被接受时。HTMLHeadElement 将被隐式建立,尽管咱们没有 “head” 标记,而且它会被添加到树中。
如今咱们移动到 “in head” 模式,而且将会到 “after head”。body 标记是再次执行的,HTMLBodyElement 被建立和插入,模式被转换为 “in body”。
“Hello world” 字符串的字符标记如今接受了。首先会发生建立和 “Text” 模式的插入,而且其余字符也将加入到这个节点中。
body 结束标记引发到 “after body” 模式的转换。咱们会收到 html 结束标记,它将会移动到 “after after body” 模式。接受文件标记的结束将会结束解析过程。
在这个阶段,浏览器将会做为交互而标记文档,而且开始解析在 “deferred” 模式下的脚本:这些本应该在文档解析后被解析。文档状态将被设置为 “complete”而后一个 “load” 事件将被触发。
在这里能看到 HTML5 规范中标记器和树构造器的完整算法
你不会在 HTML 页面获得一个 “无效语法” 的错误。浏览器会修复任何无效的内容,而后继续。
好比下面的例子:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
复制代码
我必定要违反不少规则(“mytag” 不是个标准标准标签,“p” 和 “div” 标签的错误嵌套等等)可是浏览器仍然正确展现,而且没有任何抱怨。觉得不少解析代码在修复 HTML 做者的错误。
错误处理是十分一致的,但吃惊的是,它不是 HTML 规范的部分。如同书签和后退前进按钮同样,它只是这些年在浏览器中被发开出来。不少网站上有不少无效的 HTML 结构重复着,而且浏览器尝试用一种与其余浏览器同样的方式修复。
HTML 规范定义了一些要求。(WebKit 在 HTML 解析器类的开始的注释很好的总结了)
解析器解析标记输入成为文档,构建文档树。若是文档格式良好,解析会直接进行。
不幸的是,咱们不得不处理不少 HTML 文档,那些文档没有很好的格式,因此解析器不得不容忍这些错误。
咱们能够了解到至少如下几种错误条件:
1. 在某些标签外部,明确禁止添加元素。这种状况下,咱们应该关闭全部的标签,直到一个禁止的标签出现,以后添加它。
2. 咱们不容许直接添加元素。它多是人们写入文档忘记的标签(或者其中的标签是可选的)。好比如下标签:HTML HEAD BODY TBODY TR TD LI(漏了什么吗?)
3. 咱们想在行内元素中添加块元素。闭合全部的行内元素直到下一个更高级的块元素出现。
4. 若是这些都没有做用,直到容许咱们添加或者忽略标签才会闭合元素。
复制代码
咱们来看看 WebKit 的容错例子:
有些网站使用 </br>
而不是 <br>
。为了兼容 IE 和 火狐, 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 = new HTMLFormElement(formTag, m_document);
}
复制代码
注释不言而喻。
www.liceo.edu.mx 是一个网站的例子,这个网站签到了大约 1500 个标签层级,全部这些来自
<b>
分支。在所有忽略它们以前,咱们最多容许 20 个同类型的嵌套标签。
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;
}
复制代码
再次参考注释:
为了支持被破坏的 HTML。咱们永远不会闭合 body 标签,由于有些愚蠢的网站页面在文档真正结束以前闭合了它。咱们依赖在 end() 调用上闭合它们。
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
复制代码
因此 web 做者意识到——除非你想去表现一个 WebKit 容错代码片断做为例子——不然就写良好格式化的代码。
还记得介绍里面的解析概念吗?好吧,像 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 表明空格)被一系列选择符分割。一个规则集包含花括号和内部的描述或者可选的逗号分割。
WebKit 使用 Flex 和 Bison。如同从解析介绍中回忆起来的同样,Bison 建立了自底向上递归降低解析。火狐使用了自顶向下手工写入。这两种例子的每一个 CSS 文件会被解析成一个 StyleSheet 对象。每一个对象包含 CSS 规则。这个 CSS 规则对象包含选择器和对象声明以及其余对应 CSS 语法的对象。
web 的模型是同步的。建立者但愿当解析器遇到 <script>
标签被解析后当即执行。文档解析器停止直到脚本被执行。若是是外部脚本,那么资源首先必须从网络请求——这也是同步处理的,同时直到资源得到,不然解析一直停止。这个模型有许多年了,同时在 HTML4 和 HTML5 中被定义。创做者能够给脚本添加 “defer” 属性,在这种状况下,这将不会终止文档解析,而且在文档解析后执行脚本。HTML5添加一个可选标记给脚本做为异步标记,以便未来解析和经过不通线程执行。
Webkit 和 Firefox 都作了这种优化。当脚本执行时,另外一个线程解析剩余的文档,而且找出从网络上须要加载的其余资源而后加载它们。用这种方式,资源能够在平行链接上加载,而且总的来讲速度是提高的。注意:推断解析只解析外部资源像是外部脚本,样式表和图片:它不会修改 DOM 树——这留给主要解析器。
另外一方面样式表有着不一样的模型。概念上来讲由于它看起来并不修改 DOM 树,因此没有理由去等待他们和中止文档解析。这里有个问题,即在文档解析阶段,为样式信息的脚本请求。若是样式没有加载和解析,脚本将会获得错误答案,而且显然这会引发一系列问题。这看起来是个边缘问题,但事实上很常见。当有样式表仍然在加载和解析的时候,火狐阻止了全部的脚本。WebKit 只会阻止当尝试去访问某些样式属性,而这些属性可能被未加载的样式影响的脚本。
当 DOM 树被构建时,浏览器构建另外一个树,是渲染树。这是棵可视元素按照展现顺序排列的树,是可视化文档的表现。这棵树的目的是能够在它们正确的顺序下绘制内容。
火狐在渲染树的 “frames” 中调用元素。 WebKit 使用渲染项或者渲染对象。
渲染知道如何布局和绘制它本身以及它的后代。
WebKit的 RenderObject 类,渲染的基础类,有以下定义:
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
}
复制代码
每一个渲染表明一个矩形区域,这个区域一般对应一个 CSS 盒子节点,被 CSS2 规范描述。它包括集合图形信息像是宽高和位置。
盒子类型被相关节点(参考样式计算部分)的样式属性的 “display” 值影响。WebKit 代码决定了哪一种渲染类型应该建立为一个 DOM 节点,根据 display 的属性:
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;
}
复制代码
元素类型也会考虑:好比,表单控制和有特殊结构的表格。
在 WebKit中,若是元素想去建立特殊渲染,它会覆写 createRender()
方法。渲染指向样式对象,这个对象不包含几何信息。
渲染对应着 DOM 元素,但并非一对一的干洗。不可见的 DOM 元素不会插入到渲染树中。举个例子 “head” 元素。那些 display 值是 “none” 的元素也不会出如今树中(可是 visibility 是 “hidden” 的元素会出现)。
DOM 元素对应一些可视对象。常见的元素带有复杂的结构,它们不能经过一个矩形描述。好比: “Select” 元素有三个渲染:一个用于展现区域,一个用于下拉列表,还有一个用于按钮。当文字换行时也同样,由于宽度不知足一行,新的行必须做为额外的渲染添加。
多行渲染的另外一个例子是破坏的 HTML。根据 CSS 定义,内联元素要么只包含块元素要么只包含内联元素。在混合内容的例子中,匿名块将被建立用于包含内联元素。
一些渲染对象对应 DOM 节点,可是不在树中一样的位置上。浮动和绝对定位元素不在流上,被放在了树的不一样部分,映射在真实的结构上。一个占位结构在它们应该在的位置上。
在火狐中,表现层被当作监听注册在 DOM 更新上。表现层给 FrameConstructor
委托框架建立,而且构造器解析样式(参考样式计算)和建立框架。
在 WebKit 处理样式和建立渲染层的过程叫作 “attachment”。每一个 DOM 节点都有一个 “attach” 方法。Attachment 是同步的,节点插入给 DOM 树调用新节点的 “attach” 方法。
处理 html 和 body 标签的结果是渲染树根节点的构造。根渲染对象对应 CSS 中叫作包含块的规范:最顶部的包含其余全部块的块。它的尺寸是视窗:即浏览器窗口的区域尺寸。火狐称做 ViewPortFram
而 WebKit 称做 RenderView
。这是文档指向的渲染对象。其他的树做为 DOM 节点插入被构造。
参考 CSS2 规范的过程模型
构建渲染树须要计算每个渲染对象的可视属性。这经过计算每一个元素的样式属性来完成。
样式包括各类来源的样式表,内联样式元素和 HTML 中的可视化属性(像是 “bgcolor” 属性)。以后被翻译成匹配 CSS 样式属性。
样式表的来源有,浏览器默认样式,网页创做者提供的样式,和用户样式——这些样式表经过浏览器使用者提供(浏览器容许你定义本身喜欢的样式。在火狐中,初始化,经过放在 “Firefox Profile”文件夹中的样式完成)。
样式计算有点困难:
div div div div {}
复制代码
意味着这个规则会应用于 3 个 <div>
的后代。假设你想去检查是否这个规则应用于一个 <div>
元素。你能够选择某条树上向上路径去检查。你也能够传过节点树向上发现只有两个 div,而后规则并不适用。你这是须要去尝试另外一颗树。咱们看看浏览器如何面对这些问题:
WebKit 节点引用样式对象(RenderStyle)。这些对象能够经过节点在相同的条件下共享。这些节点是兄弟或者表兄弟而且:
火狐有两颗额外的树用于简化样式计算:一颗规则树和样式上下文树。WebKit 也有样式对象,可是没有像样式上下文树来存储,只有 DOM 节点指向相关样式。
样式上下文包含结束值。这个值被应用在全部正确顺序下的匹配规则和实行从逻辑到实际值的转换操做而计算。举个例子,若是逻辑值是屏幕上的百分比,它将被计算和转换成绝对单位。这个规则树的注意很聪明。它能够在节点中分享这些值,避免重复计算。这也节约了空间。
全部的匹配规则储存在一棵树中。路径上的底部节点有更高的优先级。树包含全部的路径,为了匹配已经发现的规则。存储这些规则是懒处理的。树不会在每一个节点开始的时候计算,但不管什么时候一个节点样式须要计算时,计算路径被添加到树中。
这个点子看树像是在词法中看单词。咱们看看已经计算的规则树:
假设咱们须要为上下文树的其余元素匹配规则,而且找出匹配规则(正确的顺序)是 B-E-I。咱们已经有在树中的路径,由于咱们已经计算出了路径 A-B-E-I-L。咱们将减小咱们的工做。
来看看树如何节约咱们的工做。
样式内容被分割成结构。这些结构包含了样式信息,像是边框和颜色这种种类。结构中的全部属性是继承或者不继承的。除非元素定义了继承属性,不然从父级继承。若是没有定义继承属性,使用默认值。
当为某个元素计算样式上下文时,咱们首先计算规则树中的路径或者使用已经存在的。接着咱们开始在路径中应用规则去在咱们新的样式上下文中应用规则。咱们开始从路径底部节点——这个节点由最高的优先级(一般是最特殊的选择器)和穿过树到顶部直到咱们的结构被填满。若是在规则节点的结构上没有定义,咱们能够很好地优化——咱们到树上直到咱们发现一个节点,是充分定义和简单指向它——这是最好的优化——真个结构被共享。这节约了末尾值的计算和内存。
若是咱们发现部分定义,咱们到树上填满。
若是在结构上找不到任何定义,有些例子中结构是 “继承” 类型,咱们在上下文树中指向咱们父级的结构。在有些例子中咱们也成功共享告终构。若是默认值被使用它就是重置结构。
若是大部分节点没有添加值,那么咱们须要作一些额外的计算,为它转换成实际值。咱们接着在树节点中缓存结果为了让后代使用。
此例中元素有一个兄弟节点,它指向同一个树节点,那么所有样式上下文能够在它们之间共享。
假定咱们有以下 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>
复制代码
和如下规则:
div {margin:5px;color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
复制代码
为了简化这些事咱们须要只填满这两种结构:颜色结构和边距结构。颜色结构包括只包括一个成员:颜色。边距结构包含四个方面。
这个结果规则树看起来是这样(节点被节点名称标记:它们指向规则的数量):
上下文树将看起来像这样(节点名:它们指向规则节点):
假设咱们解析 HTML 而且获得第二个 <div>
标签。咱们须要为这个节点建立样式上下文和填充它的样式结构。
咱们匹配这些规则而且找出 <div>
匹配规则是 1,2 和 6的。这意味着在树中已经有存在的路径,咱们的元素可使用这些路径,而且咱们为规则 6 只须要添加另外一个节点给它(在规则树中的节点 F)。
咱们将建立规则上下文而且在上下文树中放置。新的上下文内容将会在规则树中指向节点 F。
咱们须要填充样式结构。咱们从填满边距结构开始。由于最后一个规则节点(F)没有添加到边距结构中,咱们能够在树上直到找到在以前节点插入的缓存结构而后使用它。咱们将发现它在节点 B 上,它是定义的边距规则的最高节点。
在第二个 <span>
元素上的工做相对容易。咱们匹配到规则而后得出指向规则 G 的结论,像是以前的 span。由于咱们有一个兄弟节点指向相同的节点,咱们能够共享所有样式上下文,而后指向以前 span 的上下文。
对于集成自父级的包含规则的结构,缓存在上下文树中被处理(颜色属性其实是继承,可是火狐当作默认对待而后缓存在规则树上)。
若是咱们在段落里为字体添加规则做为实例:
p {font-family: Verdana; font size: 10px; font-weight: bold}
复制代码
接着这个段落元素,它是在上下文树的div的子代,做为它的父级它能够共享相同字体。这是在段落中没有字体规则定义的状况。
在 WebKit 中,那些没有规则树的,匹配声明会转化四次。首先非重要高级优先权属性被应用(属性应该是第一次应用由于其余依赖它们,好比 display),接着高优先级重要的,接着是常规优先级不重要的,最后是常规优先级重要规则。这意味着根据正确的层叠规则属性出现四次。最后的获胜。
来总结是:共享样式对象(所有和部分的内部结构)处理问题在 1 和 3。火狐规则树也会帮助在正确的顺序下应用规则。
这里有几个样式规则的来源:
p {color: blue}
复制代码
<p style="color: blue" />
复制代码
<p bgcolor="blue" />
复制代码
最后两个对元素来讲是简单匹配,由于它自身的样式属性和 HTML 属性能够做为 key 映射到使用的元素。
注意以前的问题 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 表和标签表中找到相关规则。剩下的工做只是找出哪些根据键提取的规则是真正的匹配了。
好比 div 规则以下:
table div {margin: 5px}
复制代码
它会从类表中提取,由于键是最右边的选择器,可是它不会匹配咱们的 div 元素,它没有 table 祖先。
WebKit 和 火狐都作了这种操做。
样式对象有对应每个可视化属性(全部的 CSS 属性但更通用)的属性。若是一个属性没有被任何匹配的规则定义,这些属性能够经过父级元素样式对象来继承。其余属性使用默认值。
当有更多的定义时问题就来了——这时候须要层叠顺序来解决这个问题。
样式属性的声明可能会出如今多个样式表中,或者在一个样式表中声明数次。这意味着应用工做的顺序是很重要的。这叫作 “层叠” 顺序。根据 CSS2 定义,层叠顺序是(从低到高):
浏览器声明是不重要的,当用户只有把声明标记为 important 时才会覆盖创做者的内容。一样顺序的声明会根据定义来排序,而后在根据指定的顺序。HTML 可视属性被转换成匹配 CSS 声明。它们被当作低权限的创做者规则。
选择器的明确性经过以下 CSS2 规范来定义:
链接这四个数字 a-b-c-d(在大基数进制的数字系统),获得明确性。
基于你须要使用的进制取决于在某个类目中定义的最多的数量。
举个例子,若是 a = 14 你须要使用 16 进制。不太可能的状况是 a = 17 的时候你须要使用 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 为规则覆写 “>” 操做实现排序。
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;
}
复制代码
WebKit 使用一个标记,记录全部最高层加样式表(包括 @imports)是否加载完毕。若是在 attaching 中没有彻底加载,使用占位符而且在文档中标记,而后一旦样式表被夹在会从新计算。
当渲染被建立和添加到树中后,它尚未位置和尺寸。计算这些值的过程叫作布局和重绘(reflow)。
HTML 使用的流基于布局模型,意味着在大多数状况下一次计算就能够获得几何值。流以后的元素通常不会影响流以前的元素的几何性质,因此经过文档布局能够从左只有,从上到下的执行。有个例外:好比,HTML 表格可能会要求屡次遍历(3.5)
坐标系统是相对于根框架的。使用上左位置的坐标。
布局是个递归过程。它在根节点渲染开始,也就是对应 HTML 文档的 <html>
元素。布局经过一些或者所有框架层级,为每次要求的渲染计算几何信息。
根渲染的位置是 0,0 而且它的尺寸是浏览器窗口的可见部分的视窗。
全部的渲染都有 “layout” 和 “reflow” 方法,每一个渲染调用后代须要布局的布局方法。
为了对每一次小改变不作充分的布局,浏览器使用 “dirty bit” 系统。改变或者给它本身和后代添加标记的渲染视为 “dirty”:须要布局。
有两种标记:“dirty” 和 “children are dirty”, 这意味着尽管渲染本身是合适的,它至少有一个子代须要布局。
布局在整个渲染树上能被触发——这是“全局”布局。它能做为如下结果发生:
布局是增量的,只有脏渲染会布局(这回引发一些额外布局的损失)。
增量布局当渲染是脏的时候触发(异步的)。举个例子,在从网络的额外内容被添加到 DOM 树后新的渲染将被添加到渲染树。
增量布局是经过异步完成的。火狐为增量布局将 “回流命令”排队,同时调度者会触发这些命令批量执行,而后“脏”渲染被布局。
脚本请求了样式顺序,像是“offsetHeight”能够同步地触发增量布局。
全局布局一般被同步触发。
有时由于一些属性,好比滚动位置改变,布局会在初始化布局以后做为回调触发。
优化当布局被 “resize” 触发时,或者渲染位置改变(不是尺寸),渲染尺寸从缓存中获取而且不会从新计算。
在一些例子中只有一个子树被修改,而且布局没有从根节点开始的话。这地方只会发生本地改变不会影响它的周围——好比文本被插入进文本域(不然每次敲击将会从根节点触发布局)。
布局一般有如下默认:
火狐使用一个 “state” 对象(nxHTMLReflowState)做为布局参数(记为“reflow”)。在它们之间这个状态包括父级宽度。火狐的布局输出是 “metrics” 对象(nsHTMLReflowMetrics)。它将包含渲染计算的高度。
渲染器的宽度使用包含块的宽度来计算,渲染样式 “width” 属性是 margin 和 border。
好比下面这个 div 的宽度:
<div style="width: 30%"/>
复制代码
经过 WebKit 计算可能以下(RenderBox 类 的 calcWidth 方法):
包含的宽度是包含块可用宽度的最大值或 0.这个能够用宽度在例子中是这样被计算的内容宽度:
clientWidth() - paddingLeft() - paddingRight()
复制代码
客户端宽度和客户端高度表明一个包括边距和滚动调的对象的内部
元素宽度是样式属性 “width”。它能够计算成绝对值,经过计算容器宽度的百分比。
水平边框和补白被添加。
目前这种这是“完美宽度”的计算。如今最小和最大宽度被计算。
若是最佳宽度比最大宽度更大,这个最大宽度将被使用。若是小于最小宽度(最小的不可破坏的单位)那么最小宽度被使用。
只被缓存以防布局使用,可是宽度不会改变。
当渲染到布局的中间决定它须要换行时,渲染中止而后扩散须要换行布局的父级。父级建立额外的渲染,而后在上面调用渲染。
在绘制阶段,渲染被传递,而且渲染的 “paint()” 方法被调用用于在屏幕上展现内容。绘制使用 UI 基础组件。
如同布局,绘制也是全局的——整棵树被绘制——或者增长。在增长绘制中,一些渲染在不影响整颗树的状况下改变。这个改变的渲染在屏幕上使它的矩形失效。这是由于操做系统把它当作一块“脏区域”,同时生成了“绘制”事件。操做系统聪明的合并几个区域变成一个。在 Chrome 中,这比较复杂由于渲染在主过程当中有不一样的过程。Chrome 某些程度模拟了操做系统的行为。表现层监听了事件而且代理渲染根部的消息。树被传递直到相关渲染到达。它将重绘本身(和一般它的子代)。
CSS2 定义了绘制过程的顺序。实际上这个顺序是元素在内容栈的存储的地方。由于栈渲染从后向前,因此这个顺序影响绘制。一个块的栈顺序渲染是:
火狐遍历渲染树,而后为绘制矩形构建展现列表。它包含渲染层相关的矩形,在正确的绘制顺序下(渲染层的背景和边框等等)。这种方式树为一次重绘只会传递一次而不是数次——绘制全部的背景和图片,而后是边框等等。
火狐经过不添加将被隐藏的元素优化过程,像是元素彻底在其余不透明元素下方。
在重绘前,WebKit 储存旧的矩形做为位图。这样只会渲染在新旧矩形之间的变化。
在响应变化时,浏览器尝试最小化的可能的行为。因此改变一个元素的颜色只会引发元素重绘。改变元素的位置会引发元素和它的子代或可能的兄弟节点的布局和重绘。添加一个节点将引发节点的布局和重绘。主要的变化,像是增长 “html” 元素的字号,将会引发缓存失效,整个树的重布局和重绘制。
渲染引擎是单线程的。几乎全部的事,除了网络操做,都发生在单线程中。在火狐和 Safari 中这是浏览器的主线程。在 Chrome 中,tab 过程是主线程。
网络操做能够经过几个平行线程执行。平行连接数是受限的(2-6 个连接)。
浏览器主要线程是时间循环。这是一个保持过程活动的无限循环。它等待事件(像是布局和绘制事件)而后执行它们。下面是火狐代码的主要事件循环:
while (!mExiting)
NS_ProcessNextEvent(thread);
复制代码
根据 CSS2 定义,canvas 条款描述 “格式化结构渲染的空间”:是浏览器绘制内容的地方。canvas 对每一个空间的尺寸是无限的,可是浏览器基于视窗的尺寸选择一个初始化的宽度。
根据 www.w3.org/TR/CSS2/zin…,canvas 是透明的,若是包含其余内容,然而浏览器定义一个颜色若是它没有定义的话。
CSS 盒模型描述了一个矩形盒子,它在文档中为元素生成,同时根据可视化格式模型布局。
每一个盒子有一个内容面积(好比文本和图片等等),同时可选有间距,边框,和边距面积。
每一个节点生成 0 到 n 和盒子。
全部的元素都有 “display” 属性,来定义将被生成的盒子类型。好比:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
复制代码
默认是 inline,可是浏览器样式表可能设置其余默认。举个例子:“div” 元素的默认展现是 “block”。
你能够在这里查看默认样式表的例子:www.w3.org/TR/CSS2/sam…
有三种方案:
定位方案经过设置 “position” 属性和 “float” 属性。
在静态定位中,没有位置被定义,而且使用默认位置。在其余方案中,创做者定义位置用:上下左右。
盒子布局的方式取决于:
块状盒子:在浏览器窗口中有本身的矩形的一种盒子形式。
内联盒子:没有本身的块,可是内部有内容块。
块是一个接一个的格式化垂直。内联是格式化水平。
内联盒子内部有行或者 “line boxes”。行至少与最高的盒子同样高并且能够比它更高,当盒子对齐在 “baseline”时——意味着底部元素部分对齐另外一个元素的底部。若是容器宽度不够,内联会换行。这个一般发生在段落中。
相对定位-像一般同样定位,而后根据变化移动。
一个浮动盒子漂移到行的左侧或者右侧。这个有趣的特性会让其余盒子围绕着它。
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
复制代码
看起来像这样:
布局精肯定义忽略正常流。元素不参与正常流。这个尺寸相对于容器。在固定定位中,容器是视窗。
注意:固定盒子在文档滚动的时候不会移动。
这个经过 CSS 的 z-index 属性定义。它表明沿 “z轴” 的第三维度。
盒子被分割成“栈”(称做栈内容)。每一个栈后面的元素将会先画在元素顶部,更接近用户。在重叠的例子中,最前的元素将会隐藏较前的元素。
栈根据 z-index 属性排序。从本地栈中盒子有 ‘z-index’ 属性。视窗在最外部栈。
好比:
<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>
复制代码
结果是:
尽管红色 div 在构建上高于绿色的,它可能在常规流以前,它的 z-index 属性 更高,因此在根盒子持有的栈中更向前。