作为一名web开发人员,了解浏览器的内部运作会帮助你做出更好的决定和了解最佳实践背后的原理。这是一篇相当长的文章,我们建议你花费一些时间专研,我们保证你会得到意外的收获。
浏览器可能是目前使用最广的软件,在这里,我将解释它们在视窗背后是如何运行的。当你在地址栏中输入google.com,直到页面中显示google主页,这其中到底发生了什么。
我们讨论的浏览器
如今使用的5款主要浏览器-ie、firefox、safari、chrome和opera。我将从开源浏览器-firefox、chrome和部分开源的safari中给出一些实例。通过statCounter浏览器分析,这三款浏览器在整个市场60%的份额(2011\8)。所以如今的开源浏览器是商业浏览器的组成部分。
浏览器的主要功能
浏览器的主要功能是显示你选择的资源,通过请求从服务器端返回宾显示在浏览器视窗中,这资源通常是一个网页,但也有可能是pdf、图片或者其他类型。通过统一资源定位符来指定资源位置。
浏览器解释和显示方式是通过文档和样式说明书来指定的。这些说明书通过w3c 组织(致力于web标准化组织)来维护。
这些年的发展,浏览器厂商仅仅遵循说明书中的部分细节,并且开发了各自的扩展。对于web开发者来说就导致很严重的兼容性问题。今天大部分的浏览器还是或多或少的遵循这些说明书。
各浏览器之间的用户界面有着大量共同之处,其中共同的用户界面元素如下:
<ol>足够策略,浏览器用户界面并没有在任何官方的说明书中指定,而是源自多年的用户体验和浏览器之间的模仿。Html5说明书中也没有定义应该有哪些界面元素,但是却列出一些共同的元素,其中包括地址栏、状态栏和工具栏。当然,还有些在指定的浏览器中的一些独特特性,如firefox的下载管理。
浏览器的高级结构
浏览器的主要部分
<ol> 数据存储-这是持久层。浏览器需要在硬盘上保存各种类型的数据,如cookies。Html5说明书定义了web
database,是一个轻量级的完整浏览器端数据库
重要的是要记住 chrome浏览器,不像大部分的浏览器,渲染引擎产生多个实例,对于每一个tab都是一个独立的过程。
-------------------------------------更新----------------------------------
第二章
渲染引擎
渲染引擎的责任就是将渲染完的放在浏览器窗口中以显示请求的数据。
渲染引擎默认是能显示html、xml和图片,并且通过一些插件可以显示其他类型资源,如使用 a PDF viewer插件显示PDF格式。然而,本章我们将集中在这个主要例子上。使用css格式化显示html片段和图片。
我们参考的浏览器是-firefox、chrome和safari构造在上面的两种渲染引擎上。Firefox使用是自家生产的mozilla渲染引擎-gecko,Safari和chrome都使用webkit.
Webkit是一个开源的渲染引擎,当初只是作为 liunx平台的引擎,通过苹果公司的修改开始支持mac和window系统。
主要流程
渲染引擎从网络层开始获取请求文档的内容,通常在8k字节的数据块内完成。
基本的渲染引擎流程是:
渲染引擎开始解析html片段之后将标签转换成称之为内容树的dom节点中。接着解释外部引用和内联使用的样式数据。最后样式信息和文档中可视化结构一起被用于创建另一棵渲染树`。
渲染树包括可视化的矩形属性,如颜色和大小。使用矩形是为了在浏览器窗口正确显示。
渲染树构建之后,渲染树将通过”layout(布局)”处理。这意味着要给出每一个节点在浏览器窗口中的准确位置。接下来的策略就是绘制-渲染树将被反转,使用UI后端层绘制每一个节点。
重要的是要理解这是一个渐进的过程。为了更好的用户体验,渲染引擎将竟可能的在浏览器中显示内容。一直等到html片段开始创建和布局渲染树之后开始解析。部分内容将会被解析和显示,直到剩下的内容从网络返回回来之后在继续解析。
主流程例子:
图:webkit 主流程
图:mozilla gecko渲染引擎主流程
gecko调用可视化、格式化“框架树”。每一个元素都是一个框架。Webkit使用的术语是“渲染树”,并包括“渲染对象”。Webkit使用的“布局”术语来代替在gecko中称之为”回流 reflow”。“attachment链接”是webkit术语中链接 dom节点和可视化信息(css样式)创建渲染树。相对于gecko使用一个准确的层在html片段和dom树之间这是一个次要非语义的差别。Gecko 称之为“内容渗透(content sink)”也是创建dom元素的工厂,我们将讨论其中每一个部分。
--------------------------------------更新线-------------------------------------------------------
第三章
解析
在渲染引擎中解析是一个很精密的过程,接下来我们从更深的角度来解释。让我们从简短介绍解析开始。
解析一个文档意味着将它翻译成被理解、可使用有意义的结构。解析的结果是使用节点树展示文档的结构,被称之为词法树或者语法树。
例子:解析2+3-1的表达式生成树的过程
图:数学表达式的树节点
语法
解析是基于语法规则,文档遵循一些被定义下来的语言和格式。所解析的格式必须是有明确包括词汇和语法规则的语法,称之为“上下文空闲语法context free grammar”。人类语言并如此,因此不能使用传统的解析技术来进行解析。
解析-词法分析结合(lexer combination)
解析可以被分成两个子处理过程-词法分析和语法分析
词法分析是将输入信息切成小令牌(token)的过程。小令牌是语言词汇,即有效建筑模块的集合。相对于人类语言,它包括各种出现字典中的单词。
语法分析是语法规则的应用。
解析一般是在两个部分中切分工作。一个是词法分析(有时候称之为令牌化)负责将输入的信息切分成有效的令牌,另一个是解释,负责通过语言语法规则分析文档结构来构造解析树。这个词法分析知道如何剥夺不相干的字符,如空格和换行符。
图:源代码解析树
解析是一个递归过程。解析通常对于一个新令牌要求词法分析并且尝试用其中一个语法规则去匹配。如果某一个规则被匹配,该令牌与节点保持一致添加到解析树中,然后解析将使用另一个令牌。
如果规则没有被匹配,解析内部存储该令牌,然后不断使用令牌,直到一个匹配能匹配所有内部储存的令牌规则被找到。如果该规则没有被找到,解析将会抛出异常。这就意味着文档不是有效的,是有语法错误的。
翻译
很多时候解析树并不是最终产品,解析经常被用作翻译-将输入的文档转变成另一种格式。编译就是一个例子。编译是将源代码编译成机器码。这一过程首先是将源代码解析成解析树然后将解析树翻译成机器码文档。
解析例子
在图5中,我们从一个数学表达式中创建了一个解析树。让我们在试着定义了一个简单的数学语言然后在了解这一解析过程。
词汇:我们语言包括整数、加号和减号。
语法:
让我们分析下2+3-1这个输入
第一个字串匹配的规则是2,通过规则5我们知道是一个术语。第二个字串匹配的是2+3,匹配的是第三个规则-即一个术语跟随一个操作符跟随另一个术语。接下来匹配仅仅击中了最后的输入。2+3-1是一个表达式,因为我们已经知道2+3是一个术语,所以我们有一个术语跟随一个操作符跟随另一个术语。2++将不会匹配任何规则,因此是一个无效的输入。
正式定义的词汇和语法
词汇通常使用正则表达式表达.
例如:我们的语言定义如下:
整数:0|[1-9][0-9]*
加号:+
减号:-
正如你看到的,整数使用正则表达式定义
语法通常用BNF格式定义。我们的语言定义如下:
表达式 := 术语 操作符 术语
操作符 := 加号|减号
术语:= 整数|正则表达式
我们说如果语言的语法是一个上下文空间语法(context frees grammar),那么该语言会被一般解析规则(regular parsers)所解析。上下文空间语法的直觉定义(intuitive definition)是一个完全用BNF表达的语法。
解析类型
有两种基本的解析类型-自上向下解析和自下向上解析。一个直觉的解析是自上向下看作是语法的高级别结构然后去匹配其中的每一个规则。自底向上解析从输入信息开始然后渐渐的将输入的信息翻译进语法规则中,从低级规则开始直到被高级规则匹配。
让我们看下这两种类型解析范例:
自顶向下解析从高级别规则开始-识别出2+3并作为一个表达式,然后在识别出2+3-1也作为一个表达式(识别表达式的过程进行匹配其他规则,但是开始点是最高级别规则)
自底向上解析扫描输入的信息直到被某一条规则匹配,然后用该规则取代匹配到的信息,在此继续直到匹配到最后的数据。解析棧会取代部分的匹配表达式。
自底向上的解析类型又被称之为”a shift-reduce”解析,因为输入被转向正确(想象一个指针指向第一个输入开始位置,并且移到右边去)并且渐渐减少语法规则。
一般自动化解析
有一些工具可以为你产生解析,称之为解析产生。反馈你所定义语言语法-包括词汇和语法规则-他们产生工作解析(working parser).创建这个解析需要对解析有一个深层次的理解,手动创建一个优化解析也并非易事,所以解析产生是非常有用的。Bison的输入信息是用BNF格式的语言语法规则。
Webkit使用了著名的解析产生,flex用于产生词法分析器和bison用于创建解析器(你也可以使用lex和yacc)。Flex输入信息是一个文件包括定义了各种令牌的正则表达式。Bison的输入信息是用BNF格式的语言语法规则。
Html解析
Html解析的工作是将html标记解析成一棵解析树。
Html语法定义
Html 的词汇和语法是有w3c组织起草的说明书中定义的。当前版本是html4和正在进行中的html5.
并非是一个上下文空闲语法
正如我们在解析介绍中的那样,语法可以使用BNF 类似的正式格式去定义。
不幸的是,并不是所有常规的解析主题(the conventional parser topics)都能应用到html。Html不能被很容易的通过解析所需要的上下文空闲语法所定义。
有一种正是的格式用于定义html.-DTD(文档类型定义)-而并非是上下文空闲语法。
第一次不自在的出现(appears strange at first sight).html是非常接近于xml。存在很多可用的xml解析器。也有一个html的xml变化版本-xhtml,那么他们有什么差别呢?
不同在于html技术更加“宽恕”(forgiving)。他默许你含蓄的忽略(omit)本应该添加的适当标签,有时候忽略开始或者结尾的标签等等。总而言之,html是一个温柔的语法,相对于xml的严谨(stiff)和命令式的语法。
表面上这看起来小小的不同却让世界发生了不同。一方面这也是html如此流行的主要原因-html 宽恕你的错误和让web开发者的生活变得简单。另一方面,编写正式的语法变得困难起来。总结来说,html不能被很容易的解析,自从语法不是上下文空闲语法就不能通过传统解析,,也不能通过xml解析。
Html文档类型定义
Html 定义是用文档类型定义格式。这种格式被用作定义SGML家族语言。这种格式包括定义所有容许的元素,他们的属性和层级。但是我很早就知道, html文档类型并不是来自于上下文空闲语法。
文档类型定义有很多变种。严厉模式(strict mode)完全遵循说明书但是其他模式包括曾经浏览器所使用支持的标记。目的就是要向后兼容(backwards compatibility)老的内容。
DOM
输出树-解析树是一棵dom元素和属性节点树。Dom是文档对象模型的简称。是html文档和html元素面向外部世界的接口(如javascript)的对象展示。
树的根节点是文档对象。
Dom与标记几乎有着一对一的关联,例如,这个标记
<html> <body> <p> hello world </p> <div><imgsrc=”example.png”/></div> </body> </html>
将翻译成如下的dom树
图:标记的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不能使用一般的自顶向下或者自下向上解析。
理由是:
不能使用传统的解析技术,浏览器在解析html会创建自定义解析。
解析的算法在html5说明书中有详细的介绍。算法包括两个策略。分词和树结构。
分词是词法分析,将输入的信息解析成令牌。在html中令牌是开始标签、结束标签、属性名字和属性值。
分词器识别令牌,并将令牌交给树结构,耗尽下一个字符是为了识别下一个令牌,直到信息的结束。
图:html解析流程(html5说明书)
分词算法
算法的输出是html令牌。算法用状态机表示。每一种状态消耗一个或多个输入符,通过这些字符将更新下一种状态。当前的分词状态和树结构状态都影响决定。这意味着消耗相同的字符将放弃对于正确的下一个状态的不同结果。根据当前的状态,算法变得太复杂而不能描述全部。所以让我们来看一个简单的例子将帮助我们理解这个原则。
基础例子-分词下面的 html
<html> <body> hell0 world </body> </html>
初始化状态是”数据状态”。当遇到”<”,状态将改编成“打开状态”。消耗一个a-z字符引起”开始标签令牌”的创建,状态改变成“标签名字状态”,我们将一直保持这个状态直到遇到”>”字符。每一个字符都被添加一个新的令牌名字。在我们的例子中,html令牌使我们创建的令牌。
当到达”>”标签时,当前的令牌被发散(emitted),状态并回到“数据状态”。<body>标签被相同的步骤看待。目前来说,html和body标签都是被发散的。我们现在返回到“数据状态”消耗”hello world”中的h字符将引发 创造和一个字符的发散,反复这个过程直到遇到</body>标签中的<字符。我们会为hello world中的每一个字符发散一个字符令牌。
我们在回到“开放状态”。消耗下一个输入/将引发一个结束令牌的创建并且移向“标签名字状态”.我们将再一次保持这个状态直到遇到>标签。然后新的标签令牌将被发散出,之后我们回到“数据状态”</html>输入被看作是前一个场景。
图:令牌化处理
树构造算法
当解析被创建的时候,文档对象也会被创建。在树构造策略中,dom树会伴随文档根元素修改,元素将添加到dom树中。每个节点会通过分词而发散,通过树构造而被处理。对于每一个令牌跟dom元素有着关联的说明定义,并被每一个令牌所创建。除了添加到dom树中的元素以外,也被添加到公开元素的堆中。这个堆中用于收集嵌套错误和未必合的标签。这个算法也被描述成状态机。这个状态也称之外“插入模式”.
让我们看一下树构造的处理过程
<html> <body> hello world </body> </html>
输入到树构造策略是一个从分词策略中的一组顺序令牌。第一个模式是“初始化模式”接收到html 令牌将引起向”before html”移动模式,并且在那种模式下会再次处理这个令牌。这会引起HTMLHtmlElement元素的创建和被添加到根文档对象中。
策略将被改变为”before head”。我们接收到”body”令牌。HTMLHeadElement将被含蓄的创建,尽管我们并没有head令牌,然后会被添加到树中。
我们现在移到”in head”模式,然后到 “after head”。Body令牌被再一次处理。 HTMLBodyElement被创建、插入然后模式被转移到”in body”。
“hello world”字符令牌现在被接受。第一个将引起创建然后一个“文本”字节被插入。其他的字节将添加到那个字节中。
Body结束令牌被收到之后,将引起转移到”after body”模式。现在我们收到移动到”after afterbody”模式的html结束标签。收到文件令牌的结束,解析也就结束。
图:树结构例子
解析完成后的行为
在这个策略,浏览器将标记文档为内部行为。开始解析被”延迟”模式的脚本-当文档解析之后,这些被延迟的脚本才会被执行。文档状态被设置为”完毕”和”加载”事情会被触发。
你可以在html5说明书中看到分词和树构造的完整算法。
浏览器的错误容忍
你不会在页面中看到无效语法错误。浏览器会帮你修复任何无效内容并继续执行。
下面代码为例:
<html> <mytag> </mytag> <div> <p> </div> really lousy html </p> </html>
我已经违反了百万规则(“mytag”不是一个标准标签,p和div元素的错误嵌套)但是浏览器还是会正确的显示,并且不会抱怨。所以大量的解析代码用来修复页面开发者的错误。
错误处理与浏览器保持一致。但是吃惊的是它并不是当前html 说明书中的一部分。像书签和前进/后退按钮,这些已经在很多年前就已经被开发了。在很多网站都会报告一些很出名的无效html结构,浏览器厂商与其他厂商达成一致规则去试图修复他们。
Html5说明书并没有定义这些需求。Webkit在html类的开始的注释中总结这些。
解析器解析令牌到文档中,创建文档树。如果文档是格式良好,解析将会变得很快。
不幸的是,我们不得不去处理很多格式不好的文档,所以解析器不得不去容忍这些错误。
我们不得不关系最近如下错误条件:
让我们看一些webkit错误容忍的例子
</br>instead of <br>
一些站点使用
替代
.为了兼容ie和firefox浏览器。Webkit这样处理如
代码: if (t->isCloseTag(brTag) &&m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true; }
记住:错误处理是在内部进行的,他不会展示给用户。
一个偏离的(stray)表格
一个偏离的表格是一个表格中内嵌另一个表格并不是内嵌一个表格单元(cell)
例如:
<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标签的嵌套行为。我们只允许嵌套相同类型的标签,其余都会被忽略。
boolHTMLParser::allowNestedRedundantTag(constAtomicString&tagName) { unsignedi = 0; for (HTMLStackElem* curr = m_blockStack; i<cMaxRedundantTagDepth&&curr&&curr->tagName == tagName; curr = curr->next, i++) { } returni != cMaxRedundantTagDepth; }
错位的(misplace)的html或者body结束标签。
再注释
支持真正意思上的片段html。我们从不关闭body标签,在文档实际关闭之前,然而一些愚蠢的网页将会关闭它。我们依赖调用end方法来关闭。
if (t->tagName == htmlTag || t->tagName == bodyTag ) return;
所以网页设计师要小心(beware),如果你不想在 webkit错误容忍代码片段作为例子出现,写结构良好的html.
CSS 解析
在介绍篇中有记住解析的内容?好的,不想html、css是一个上下文空闲语法和可以使用任何类型解析器描述进行解析。事实上,样式说明书中定义了css分词和语法。
让我们看一些例子:
分析语法(单词)对每一个分词通过正则表达式进行定义。
注释 \/\*[^*]*\*+([^/*][^*]*\*+)*\/ 数字 [0-9]+|[0-9]*”.”[0-9]+ 无效(nonascii) [\200-\377] nmstart [_a-z]|{nonascii}|{escape} nmachar [_a-z]|{nonascii}|{escape} name {nmchar}+ ident {nmstart}{nmchar}*
“ident”是identifier简短 ,像class的名字。”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*] ')' ] ;
解释:A规则如下结构
div.error , a.error { color:red; font-weight:bold; }
div.error和a.error是选择器,里面的部分包含了需要应用的规则。这个结构被如下所定义。
Ruleset : selector [ ‘,’ S* selector ]* ‘{’ S* declaration [ ‘:’ S* declaration ]* ‘}’ S* ;
这意味这个规则是一个选择器或者被空格分开的可选数字选择器。一个规则集包含卷曲的花括号(curly braces)和声明或者是被分号(semicolon)分开的可选的数字声明。“声明“和”选择器”被接下来的BNF格式所定义..
Webkitcss解析器
Webkit使用flex和bison解析器产生并自动创建解析从css语法文件中。当你从解析介绍中重新回忆时,bison创建一个自底向上转换-减少解析器。Firefox使用自顶向下解析器通过手动编写。在这两种场景中,每一份css文件都被解析到样式表对象中(stylesheet object),每一个对象保存css规则.css规则对象包含选择器和声明对象和与css语法保持一致的其他对象.
图:css解析
脚本和样式表的处理命令(the order of processing …)
web模式是一个同步状态。开发者希望当解析器到达<script>标签的时候,缴费可以立即解析和执行。这个时候解析的文档会暂定直到脚本被执行完毕。如果脚本是外部引用,那么资源必须首先从网络层抓取,这也是一个同步完成,此时解析被暂停直到资源被抓取完毕。这是很多年的模式,也被定义在html4和5的说明书中。开发者可以标记脚本为defer,这样解析文档将不会暂定,并且当解析完之后脚本才会去执行。Html5中给脚本添加了可选的异步标记,不过是通过另一个线程解析和执行。
思索性的解析(speculative)
webkit和firefox都进行了优化。当执行脚本时,另一个线程解析剩下的文档。确保其他需要的资源从网络中加载过来。这种方式的资源在平行的链接加载而且整体的速度也比较好。记住-这种思索性的解析并不讳改变dom树和节点对应主解析器来说。它仅解析引用到外部资源时,如外部脚本、样式表和图片。
样式表
另一方面样式表有不同的模式。从内容上说,看似样式表不会改变dom树,也没有理由等待他们并停止文档的解析,这是一个问题,尽管在文档解析策略中脚本需要样式信息。如果样式不被加载和解析,脚本将会得到错误的回答并会引起更多的问题。这看起来是一个边缘例子,但却相当的普遍。Firefox会堵塞所有脚本当有样式表被下载和解析时。Webkit只有在试图访问当前样式属性,有可能会受到为加载样式表的影响,才会堵塞脚本。
-----------------------------------------更新线------------------------------------------------------
第四章
渲染树结构
当dom树创建好,浏览器开始构造第二棵树-渲染树。这棵树中的可视化元素将会被在该显示的地方显示。是文档的可视化再现。这棵树的目的是可以绘制内容通过正确的命令。
Firefox在渲染树的框架中(frames)调用这些元素。Webkit使用术语渲染器或者渲染对象。
一个渲染器知道如何去布局和绘制自己和他的子节点。
Webkit渲染对象类,是渲染器的基础类,有如下的定义:
Class RenderObject { Virtual void layout(); Virtual void paint(PaintInfo); Virtual void rectrepaintRect(); Node* node; //the Dom node RenderStyle* style //the computed style RenderLayer* containgLayer; //the containing z-index layer }
每一个渲染器显示一个矩形区域,通过与节点的css盒模型保持一致,在css2说明书中有描述。它包含地理位置信息如宽、高和位置。
盒模型跟每一个节点的’显示’样式属性影响有关系(请看样式计算这个章节).这里是webkit代码决定了对于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; }
元素类型也是需要考虑的,如表单元素控制和表格有特殊的框架
在webkit中,如果一个元素想去创建特殊的渲染器,他将覆盖createRender方法。这渲染器只想样式对象保存的非地理位置信息。
渲染树与dom树的联系
渲染器与dom元素始终保持联系,但这种联系并不是一对一的。非可视化元素将不会插入到渲染树中。如’head’元素。因为这些元素的显示属性被分配成’none’将不会在树中出现。(但是有’隐藏hidden’可视化属性的元素将会出现在渲染树中)。
也有一些dom元素与很多的可视化元素保持对应。这些元素通常有些复杂的结构,并不能在单一的矩形中被描述。如’下拉框’有三个渲染器-一个是显示区域,一个是下拉列表项盒模型和一个按钮。还有当一个文版被多行拆分时,因为这个宽度对于每一行不在请确,新的一行也会被加入到额外的渲染器中。另外的例子,html被很多渲染器所打破,根据css说明书一个内嵌元素必须包含要不仅仅时块级元素要不仅仅是内嵌元素(according to css spec an inline element must contain either only block element or only inline elements)在混合内容场景中,匿名的块级渲染器将被创建去包装内嵌元素。
一些渲染对象与dom节点保持一致性,但是也并非在一棵树中相同的位置。浮动和绝对定位元素将脱离文本流(floats and absolutely positioned elements are out of flow),并定位在不同位置上并映射到真实的框架中去。在他们所在的地方会有一个展位框架。
图:渲染树与dom树的关系
构造文本流的树
在firefox,显示被注册成dom更新的监听器。这种显示委派到框架的创建和结构的样式处理(看样式计算)并创建一个框架.
在webit中,处理样式的过程和创建渲染器被称作’链接attachment’。每一个dom节点有一个‘链接’方法。链接是一个同步状态,节点插入到dom树将调用新的‘链接方法’.
处理html和tag标签将会引起渲染树根的创建。这个根渲染对象与css说明书中描述的一样将会调用包含的块级。最顶端是一个包含所有其他块级的块级。它的纬度是一个可视端(viewport)-浏览器窗口显示区域纬度。Firefox称他为可视端框架(viewPortFrame)。Webkit称之为可视渲染可视(renderView).这是一个文档指向的渲染对象。剩下的树作为节点插入创建。
请看css2说明书关于盒模型的处理。
样式计算
创建一个渲染树需要计算每一个渲染对象的可视属性,这是由计算每一个元素的样式属性来完成的。
样式包括各种原始的样式表。内嵌样式元素和html中的可视化属性,如‘背景’属性,之后会转换成匹配到的css样式属性。
原始的样式属性是浏览器默认的样式表,这些样式表由开发者和用户样式表提供。用户样式表是由浏览器用户提供的。(浏览器允许你定义自己喜爱的样式,在firefox中,如通过取代firefox profile文件中样式表来完成)。
样式计算会带来新的问题:
例如-计算选择器
div div div div { …. }
意味应用到div上规则是另外三个div的祖先。假设你想检查是否这些规则应用到给定的div元素上。你选择一个适当的路径去检查。你可能需要反转节点树去确认仅有两个div并且这个规则并没有应用,然后你需要在试试这棵树的其他路径。
<ol>让我们来看下浏览器是如何面对这些问题的:
共享样式数据
webkit节点引用样式对象(渲染样式)这些对象可以被相同条件下的节点共享。这些节点是兄弟节点或者表兄弟。
<ol>firefox规则树
firefox对于早期的样式有两个额外树-规则树和上下文样式树。Webkit也有样式对象但是他们没有象上下文样式树存储,仅仅是dom节点指向相关的样式。
图:firefox上下文样式树
上下文样式保存结束值(end value)这些通过应用所匹配的规则被计算出来并且从合法转换成独立的值在表现出来。如,如果合法值是屏幕的百分比,那么它将被计算并转换成绝对单元。这规则树思想真的很职能。它能共享这些值在节点之中为了避免再一次计算他们,这也节省了空间。
这个思想是将树的路径看成是单词的词素。
假设我们需要匹配另一个元素在内容树中的规则,并且确保这条匹配的规则。我们已经有这样的一条路径在树中,因为我们已经计算a-b-e-i-l的路径,我们现在就可以做更少的工作。
让我们来看下这棵树如何保存我们的工作。
-----------------------------------------更新线------------------------------------------------------
第五章
布局
当渲染创建完成并添加到树中,但是他没有位置和大小,计算这些值被称之为布局或者回流。
Html使用基于布局模型流动,意味着大多数的时间可能用单通道计算几何图形。回流之后的元素典型的不会影响回流之前的元素,所以布局将贯穿文档从左到又,从上到下的处理。会有一些异常,如html中的table可能需要多余一个通道。
坐标系统(the coordinate system)与根框架有关,坐标中的上、左会被使用。
布局是一个递归过程。从根渲染开始,与html文档中的 html元素保持一致。尽管一些框架是嵌套结构,但是布局会继续嵌套执行,计算每一个渲染需要的几何信息。
注:根的渲染位置是从(0,0)开始的,它的纬度是一个视窗-浏览器窗口的可视区域。
所有的渲染器都有一个布局或者回流的方法,每一个渲染器调用需要布局的孩子节点的布局方法。
脏字节系统(dirty bit system)
为了不为每一个细微的改变去执行一次完全的布局,浏览器使用一个叫做‘脏字节’系统。渲染器被改变或者添加,会把自己和其孩子标记为’脏’-需要渲染。
这里有两个标记-‘脏’和‘孩子是脏’。孩子是脏意味着尽管自身渲染可能没问题,但是有至少一个孩子也是需要布局。
全局和局部布局
布局在整体渲染树中被触发-这是全局布局。发生的理由:
1、全局样式改变会影响所有的渲染器,如字体大小的改变。
2、屏幕尺寸发生变化。
布局也可以是局部,仅仅脏渲染器会布局(这可能引起一些需要额外布局的破坏)
当渲染器是脏的时候,局部渲染器会被触发(异步过程)。例如,当加载完来自网络层的额外内容之后,新的渲染器会被添加渲染树中并添加到dom树中。
图:局部渲染-仅仅脏渲染和其他孩子被渲染
异步和同步渲染
局部渲染是异步完成的。Firefox为局部布局排列一个‘回流命令’,并且会为这些命令设计一个批量触发执行的日程表。Webkit也使用一个定时器去执行局部渲染-树被反转并且脏渲染器会去布局。
脚本需要样式信息,如’offsetHeight’会异步触发局部渲染。
全局渲染通常是同步触发的。
有时候,初始化完成之后,回调也会触发布局,因为有一些属性,如滚动条位置发生改变。
优化
当布局是通过’resize’或者渲染器的位置发生改变(而非大小)触发时,渲染器大小从缓存中取得并不能计算。
在一些场景中,仅仅子树被修改,布局并不会从根节点开始。这种场景的改变只会改变是本地区域并不会影响到周边环境。
布局过程
布局通常有如下的设计:
1、父渲染器决定它自身宽度
2、父会覆盖孩子
2.1 取代孩子渲染器(设置它的 x和y)
2.2 如果需要调用孩子布局(他们是脏或者我们是全局布局或者其他理由),计算孩子们高度。
3、父使用孩子们累积的高度和外边距和内边距的高度设置自身的高度-使用父渲染器的渲染器。
4、设置它的脏字节为false.
Firefox使用‘状态’属性(nsHTMLReflowState)作为布局(术语是回流)的参数。在其他的状态之中包括父宽度。Firefox布局的输出叫做’metrics(度量)’对象(nsHTMLReflowMetrics)。它包含计算高度的渲染器。
宽度计算
渲染器的宽度是用容器块级的宽度计算的,渲染器的样式‘宽度’属性-外边距和边框。
如下面div的宽度:
clientWidth和 clientHeight展现是包括边框和滚动条的内部对象。
元素的宽度,是样式属性的宽度,通过计算容器宽度区域会计算出一个绝对值。
同一个水平线的边框和内边距会在此时添加。
计算出来的值会被缓存起来,有种场景,需要布局的话,宽度是不会改变的。
折行(line breaking)
当布局中间的渲染器决定需要折行的时候,渲染器将停止并且冒泡到它的父节点需要被折行。这个父渲染器将创建额外的渲染器并在他们之上调用布局。