简介
浏览器可以被觉得是使用最广泛的软件,本文将介绍浏览器的工作原理,我们将听到,从你在地址栏输入google.com到你看见google主页过程中都发生了哪些。
将讨论的浏览器
明天,有五种主流浏览器——IE、Firefox、Safari、Chrome及Opera。
本文将基于一些开源浏览器的事例——Firefox、Chrome及Safari,Safari是部分开源的。
依据W3C(WorldWideWebConsortium万维网联盟)的浏览器统计数据,当前(2011年9月),Firefox、Safari及Chrome的市场占有率综合已快接近50%。(原文为2009年10月,数据没有太大变化)因而,可以说开源浏览器将近抢占了浏览器市场的半壁江山。
浏览器的主要功能
浏览器的主要功能是将用户选择得web资源呈现下来,它须要从服务器恳求资源,并将其显示在浏览器窗口中,资源的格式一般是HTML,也包括PDF、image及其他格式。用户用URI(UniformResourceIdentifier统一资源标示符)来指定所恳求资源的位置,在网路一章有更多讨论。
HTML和CSS规范中规定了浏览器解释html文档的形式,由W3C组织对这种规范进行维护,W3C是负责拟定web标准的组织。
HTML规范的最新版本是HTML4(),HTML5还在拟定中(今译:三年前),最新的CSS规范版本是2(),CSS3也还正在拟定中(今译:同样三年前)。
那些年来,浏览器厂商纷纷开发自己的扩充,对规范的遵守并不健全,这为web开发者带来了严重的兼容性问题。
然而,浏览器的用户界面则差不多,常见的用户界面元素包括:
奇怪的是,并没有那个即将公布的规范对用户界面作出规定,这种是多年来各浏览器厂商之间互相模仿和不断改进得结果。
HTML5并没有规定浏览器必须具有的UI元素,但列举了一些常用元素,包括地址栏、状态栏及工具栏。还有一些浏览器有自己专有得功能,例如Firefox得下载管理。更多相关内容将在前面讨论用户界面时介绍。
浏览器的主要构成HighLevelStructure
浏览器的主要组件包括:
图1:浏览器主要组件
须要注意的是,不同于大部份浏览器,Chrome为每位Tab分配了各自的渲染引擎实例,每位Tab就是一个独立的进程。
对于构成浏览器的这种组件,旁边会逐一详尽讨论。
组件间的通讯Communicationbetweenthecomponents
Firefox和Chrome都开发了一个特殊的通讯结构,前面将有专门的一章进行讨论。
渲染引擎Therenderingengine
渲染引擎的职责就是渲染,即在浏览器窗口中显示所恳求的内容。
默认情况下,渲染引擎可以显示html、xml文档及图片,它也可以利用插件(一种浏览器扩充)显示其他类型数据,比如使用PDF阅读器插件,可以显示PDF格式,将由专门一章讲解插件及扩充,这儿只讨论渲染引擎最主要的用途——显示应用了CSS以后的html及图片。
渲染引擎Renderingengines
本文所讨论得浏览器——Firefox、Chrome和Safari是基于两种渲染引擎打造的,Firefox使用Geoko——Mozilla自主研制的渲染引擎,Safari和Chrome都使用webkit。
Webkit是一款开源渲染引擎,它原本是为linux平台研制的,后来由Apple移植到Mac及Windows上,相关内容请参考。
主流程Themainflow
渲染引擎首先通过网路获得所恳求文档的内容,一般以8K分块的形式完成。
下边是渲染引擎在取得内容以后的基本流程:
解析html以建立dom树->建立render树->布局render树->勾画render树
图2:渲染引擎基本流程
渲染引擎开始解析html,并将标签转化为内容树中的dom节点。接着,它解析外部CSS文件及style标签中的款式信息。这种款式信息以及html中的可见性指令将被拿来建立另一棵树——render树。
Render树由一些包含有颜色和大小等属性的圆形组成,它们将被根据正确的次序显示到屏幕上。
Render树建立好了以后,将会执行布局过程,它将确定每位节点在屏幕上的准确座标。再下一步就是勾画,即遍历render树,并使用UI前端层勾画每位节点。
值得注意的是,这个过程是逐渐完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成以后再去建立和布局render树。它是解析完一部份内容就显示一部份内容,同时,可能还在通过网路下载其余内容。
图3:webkit主流程
图4:Mozilla的Geoko渲染引擎主流程
从图3和4中可以看出,虽然webkit和Gecko使用的术语稍有不同,她们的主要流程基本相同。Gecko称可见的低格元素组成的树为frame树,每位元素都是一个frame,webkit则使用render树这个名词来命名由渲染对象组成的树。Webkit中元素的定位称为布局,而Gecko中称为回流。Webkit称借助dom节点及款式信息去建立render树的过程为attachment,Gecko在html和dom树之间附加了一层,这层称为内容接收器,相当制造dom元素的鞋厂。下边将讨论流程中的各个阶段。
解析Parsing-general
既然解析是渲染引擎中一个十分重要的过程,我们将稍稍深入的研究它。首先简略介绍一下解析。
解析一个文档正式其转换为具有一定意义的结构——编码可以理解和使用的东西。解析的结果一般是抒发文档结构的节点树,称为解析树或句型树。
比如,解析“2+3-1”这个表达式,可能返回这样一棵树。
图5:物理表达式树节点
文法Grammars
解析基于文档根据的句型规则——文档的语言或格式。每种可被解析的格式必须具有由词汇及句型规则组成的特定的文法,称为上下文无关文法。人类语言不具有这一特点,因而不能被通常的解析技术所解析。
解析器-词法剖析器Parser-Lexercombination
解析可以分为两个子过程——语法剖析及词法剖析
词法剖析就是将输入分解为符号,符号是语言的词汇表——基本有效单元的集合。对于人类语言来说,它相当于我们字典中出现的所有词组。
句型剖析指对语言应用语法规则。
解析器通常将工作分配给两个组件——词法剖析器(有时也叫动词器)负责将输入分解为合法的符号,解析器则按照语言的句型规则剖析文档结构,进而建立解析树,词法剖析器晓得如何跳过空白和换行之类的无关字符。
图6:从源文档到解析树
解析过程是迭代的,解析器从词法剖析器处取道一个新的符号,并试着用这个符号匹配一条句型规则,假如匹配了一条规则,这个符号对应的节点将被添加到解析树上,之后解析器恳求另一个符号。假如没有匹配到规则,解析器将在内部保存该符号,并从词法剖析器取下一个符号,直至所有内部保存的符号才能匹配一项句型规则。假如最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或是包含句型错误。
转换Translation
好多时侯,解析树并不是最终结果。解析通常在转换中使用——将输入文档转换为另一种格式。编译就是个反例,编译器在将一段源码编译为机器码的时侯,先将源码解析为解析树,之后将该树转换为一个机器码文档。
图7:编译流程
解析实例Parsingexample
图5中,我们从一个物理表达式建立了一个解析树,这儿定义一个简单的物理语言来看下解析过程。
词汇表:我们的语言包括整数、加号及加号。
句型:
1.该语言的句型基本单元包括表达式、term及操作符
2.该语言可以包括多个表达式
3.一个表达式定义为两个term通过一个操作符联接
4.操作符可以是减号或负号
5.term可以是一个整数或一个表达式
现今来剖析一下“2+3-1”这个输入
第一个匹配规则的子字符串是“2”,按照规则5,它是一个term,第二个匹配的是“2+3”,它符合第2条规则——一个操作符联接两个term,下一次匹配发生在输入的结束处。“2+3-1”是一个表达式,由于我们早已晓得“2+3”是一个term,所以我们有了一个term紧跟随一个操作符及另一个term。“2++”将不会匹配任何规则,因而是一个无效输入。
词汇表及句型的定义
词汇表一般借助正则表达式来定义。
比如前面的语言可以定义为:
INTEGER:0|[1-9][0-9]
PLUS:+
MINUS:-
正如听到的,这儿用正则表达式定义整数。
句型通常用BNF格式定义,我们的语言可以定义为:
expression:=termoperationterm
operation:=PLUSMINUS
term:=INTEGERexpression
假如一个语言的文法是上下文无关的,则它可以用正则解析器来解析。对上下文无关文法的一个直观的定义是,该文法可以用BNF来完整的抒发。可查看。
解析器类型Typesofparsers
有两种基本的解析器——自顶向上解析及自底向下解析。比较直观的解释是,自顶向上解析,查看句型的最高层结构并试着匹配其中一个;自底向下解析则从输入开始,逐渐将其转换为句型规则,从底层规则开始直至匹配高层规则。
来看一下这两种解析器怎么解析里面的反例:
自顶向上解析器从最高层规则开始——它先辨识出“2+3“,将其视为一个表达式,之后辨识出”2+3-1“为一个表达式(辨识表达式的过程中匹配了其他规则,但出发点是最高层规则)。
自底向下解析会扫描输入直至匹配了一条规则,之后用该规则代替匹配的输入,直至解析完所有输入。部份匹配的表达式被放置在解析堆栈中。
自底向下解析器称为shiftreduce解析器,由于输入往右联通(想像一个表针首先指向输入开始处,并往右联通),并逐步简化为句型规则。
手动化解析Generatingparse
解析器生成器这个工具可以手动生成解析器,只须要指定语言的文法——词汇表及句型规则,它就可以生成一个解析器。创建一个解析器须要对解析有深入的理解,但是自动的创建一个由较好性能的解析器并不容易,所以解析生成器很有用。Webkit使用两个著名的解析生成器——用于创建句型剖析器的Flex及创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的句型规则。rsautomatically
HTML解析器HTMLParser
HTML解析器的工作是将html标示解析为解析树。
HTML文法定义TheHTMLgrammardefinition
W3C组织制订规范定义了HTML的词汇表和句型。
非上下文无关文法Notacontextfreegrammar
正如在解析简介中提及的,上下文无关文法的句型可以用类似BNF的格式来定义。
不幸的是,所有的传统解析方法都不适用于html(其实我提出它们并不只是由于好玩,它们将拿来解析css和js),html不能简单的用解析所需的上下文无关文法来定义。
Html有一个即将的格式定义——DTD(DocumentTypeDefinition文档类型定义)——但它并不是上下文无关文法,html更接近于xml,如今有好多可用的xml解析器,html有个xml的变体——xhtml,它们间的不同在于,html更包容,它容许忽视一些特定标签,有时可以省略开始或结束标签。总的来说,它是一种soft句型,不像xml生硬、固执。
其实,这个看上去很小的差别却带来了很大的不同。一方面,这是html流行的诱因——它的包容使web开发人员的工作愈加轻松,但另一方面,这也使很难去写一个低格的文法。所以,html的解析并不简单,它既不能用传统的解析器解析,也不能用xml解析器解析。
HTMLDTD
Html适用DTD格式进行定义,这一格式是用于定义SGML家族的语言,包括了对所有容许元素及它们的属性和层次关系的定义。正如上面提及的,htmlDTD并没有生成一种上下文无关文法。
DTD有一些变种,标准模式只遵循规范,而其他模式则包含了对浏览器过去所使用标签的支持,那么做是为了兼容曾经内容。最新的标准DTD在
DOM
输出的树,也就是解析树,是由DOM元素及属性节点组成的。DOM是文档对象模型的简写,它是html文档的对象表示,作为html元素的外部插口供js等调用。
树的根是“document”对象。
DOM和标签基本是一一对应的关系,比如,如下的标签:
<html> <body> <p> Hello DOM p> <div><img src=”example.png” />div> body> html>
将会被转换为下边的DOM树:
图8:示例标签对应的DOM树
和html一样,DOM的规范也是由W3C组织制订的。访问,这是使用文档的通常规范。一个模型描述一种特定的html元素,可以在查看html定义。
这儿所谓的树包含了DOM节点是说树是由实现了DOM插口的元素建立而成的,浏览器使用已被浏览器内部使用的其他属性的具体实现。
解析算法Theparsingalgorithm
正如上面章节中讨论的,hmtl不能被通常的自顶向上或自底向下的解析器所解析。
缘由是:
1.这门语言本身的包容特点
2.浏览器对一些常见的非法html有容错机制
3.解析过程是往复的,一般源码不会在解析过程中发生改变,但在html中,脚本标签包含的“document.write”可能添加标签,这说明在解析过程中实际上更改了输入
不能使用正则解析技术,浏览器为html订制了专属的解析器。
Html5规范中描述了这个解析算法,算法包括两个阶段——符号化及建立树。
符号化是词法剖析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。
符号辨识器辨识出符号后,将其传递给树建立器,并读取下一个字符,以辨识下一个符号,这样直至处理完所有输入。
图9:HTML解析流程
符号辨识算法Thetokenizationalgorithm
算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并按照这种字符转移到下一个状态,当前的符号状态及建立树状态共同影响结果,这意味着,读取同样的字符,可能由于当前状态的不同,得到不同的结果以步入下一个正确的状态。
这个算法很复杂,这儿用一个简单的事例来解释这个原理。
基本示例——符号化下边的html:
<html> <body> Hello world body> html>
初始状态为“DataState”,当遇见“”,每位字符都附加到这个符号名上,事例中创建的是一个html符号。
当读取到“>”,当前的符号就完成了,此时,状态回到“Datastate”,“”重复这一处理过程。到这儿,html和body标签都辨识下来了。如今,回到“Datastate”,读取“Helloworld”中的字符“H”将创建并辨识出一个字符符号,这儿会为“Helloworld”中的每位字符生成一个字符符号。
这样直至遇见“”中的“”。之后,形成一个新的标签符号并回到“Datastate”。前面的“”将和“”一样处理。
图10:符号化示例输入
树的建立算法Treeconstructionalgorithm
在树的建立阶段,将更改以Document为根的DOM树,将元素附加到树上。每位由符号辨识器辨识生成的节点将会被树构造器进行处理,规范中定义了每位符号相对应的Dom元素,对应的Dom元素将会被创建。这种元素不仅会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈拿来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。
来看一下示例中树的创建过程:
<html> <body> Hello world body> html>
建立树这一阶段的输入是符号辨识阶段生成的符号序列。
首先是“initialmode”,接收到html符号后将转换为“beforehtml”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。
状态此时变为“beforehead”,接收到body符号时,虽然这儿没有head符号,也将手动创建一个HTMLHeadElement元素并附加到树上。
如今,转入“inhead”模式,之后是“afterhead”。到这儿,body符号会被再度处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“inbody”模式。
之后,接收到字符串“Helloworld”的字符符号,第一个字符将造成创建并插入一个text节点,其他字符将附加到该节点。
接收到body结束符号时,转移到“afterbody”模式,接着接收到html结束符号,这个符号意味着转移到了“afterafterbody”模式,当接收到文件结束符时,整个解析过程结束。
图11:示例html树的建立过程
解析结束时的处理Actionwhentheparsingisfinished
在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。
文档状态将被设置为完成,同时触发一个load风波。
Html5规范中有符号化及建立树的完整算法(#html-parser)。
浏览器容错Browserserrortolerance
你从来不会在一个html页面上见到“无效句型”这样的错误,浏览器修补了无效内容并继续工作。
以下边这段html为例:
<html> <mytag> mytag> <div> <p> div> Really lousy HTML p> html>
这段html违背了好多规则(mytag不是合法的标签,p及div错误的嵌套等等),并且浏览器依然可以没有任何怨言的继续显示,它在解析的过程中修补了html作者的错误。
浏览器都具有错误处理的能力,然而,另人吃惊的是,这并不是html最新规范的内容,如同书签及前进退后按键一样,它只是浏览器常年发展的结果。一些比较著名的非法html结构,在许多站点中出现过,浏览器都试着以一种和其他浏览器一致的方法去修补。
Html5规范定义了这方面的需求,webkit在html解析类开始部份的注释中做了挺好的总结。
解析器将符号化的输入解析为文档并创建文档,但不幸的是,我们必须处理好多没有挺好低格的html文档,起码要当心下边几种错误情况。
1.在未闭合的标签中添加明晰严禁的元素。这些情况下,应当先将前一标签闭合
2.不能直接添加元素。有些人在写文档的时侯会忘了中间一些标签(或则中间标签是可选的),例如HTMLHEADBODYTRTDLI等
3.想在一个行内元素中添加球状元素。关掉所有的行内元素,直至下一个更高的球状元素
4.假如那些都不行,就闭合当前标签直至可以添加该元素。
下边来看一些webkit容错的事例:
br>替代<br>
一些网站使用
代替
,为了兼容IE和Firefox,webkit将其看作
。
代码:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) { reportError(MalformedBRError); t->beginTag = true; }
Note-这儿的错误处理在内部进行,用户看不到。
走失的表格
这指一个表格嵌套在另一个表格中,但不在它的某个单元格内。
例如下边这个反例:
<table> <table> <tr><td>inner tabletd>tr> table> <tr><td>outer tabletd>tr> table>
webkit将会将嵌套的表格变为两个兄弟表格:
<table> <tr><td>outer tabletd>tr> table> <table> <tr><td>inner tabletd>tr> table>
代码:
if(m_inStrayTableContent&&localName==tableTag)
popBlock(tableTag);
webkit使用堆栈储存当前的元素内容,它将从外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。
嵌套的表单元素
用户将一个表单嵌套到另一个表单中,则第二个表单将被忽视。
代码:
if (!m_currentFormElement) { m_currentFormElement = new HTMLFormElement(formTag, m_document); }
太深的标签承继
是一个由嵌套层次的站点的反例,最多只容许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闭合标签
又一次不言自明。
支持不完整的html。我们从来不闭合body,由于一些荒谬的网页总是在还未真正结束时就闭合它。我们依赖调用end方式去执行关掉的处理。
代码:
if (t->tagName == htmlTag t->tagName == bodyTag ) return;
所以,web开发者要当心了,除非你想成为webkit容错代码的范例,否则还是写格式良好的html吧。
CSS解析CSSparsing
还记得简介中提及的解析的概念吗,不同于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”是辨识器的简写,相当于一个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*] ‘)’ ];
说明:一个规则集合有这样的结构
div.error , a.error { color:red; font-weight:bold; }
div.error和a.error时选择器,大括弧中的内容包含了这条规则集合中的规则,这个结构在下边的定义中即将的定义了:
ruleset : selector [ ',' S* selector ]* ‘{’ S* declaration [ ';' S* declaration ]* ‘}’ S* ;
这说明,一个规则集合具有一个或是可选个数的多个选择器,这种选择器以冒号和空格(S表示空格)进行分隔。每位规则集合包含大括弧及大括弧中的一条或多条以分号隔开的申明。申明和选择器在前面进行定义。
WebkitCSS解析器WebkitCSSparser
Webkit使用Flex和Bison解析生成器从CSS句型文件中手动生成解析器。追忆一下解析器的介绍,Bison创建一个自底向下的解析器,Firefox使用自顶向上解析器。它们都是将每位css文件解析为款式表对象,每位对象包含css规则,css规则对象包含选择器和申明对象,以及其他一些符合css句型的对象。
图12:解析css
脚本解析Parsingscripts
本章将介绍Javascript。
处理脚本及款式表的次序Theorderofprocessingscriptsandstylesheets
脚本
web的模式是同步的,开发者希望解析到一个script标签时立刻解析执行脚本,并阻塞文档的解析直至脚本执行完。假如脚本是外引的,则网路必须先恳求到这个资源——这个过程也是同步的,会阻塞文档的解析直至资源被恳求到。这个模式保持了好多年,但是在html4及html5中都非常指定了。开发者可以将脚本标示为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5降低了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。
预解析Speculativeparsing
Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载前面须要通过网路加载的资源。这些方法可以使资源并行加载因而使整体速率更快。须要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,例如外部脚本、样式表及图片
款式表Stylesheets
款式表采用另一种不同的模式。理论上,既然款式表不改变Dom树,也就没有必要停下文档的解析等待它们,但是,存在一个问题,脚本可能在文档的解析过程中恳求款式信息,假如款式还没有加载和解析,脚本将得到错误的值,即便这将会造成好多问题,这看上去是个边沿情况,但确实很常见。Firefox在存在款式表还在加载和解析时阻塞所有的脚本,而chrome只在当脚本企图访问个别可能被未加载的款式表所影响的特定的款式属性时才阻塞这种脚本。
渲染树的构造Rendertreeconstruction
当Dom树建立完成时,浏览器开始建立另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,建立这棵树是为了以正确的次序勾画文档内容。
Firefox将渲染树中的元素称为frames,webkit则用renderer或渲染对象来描述这种元素。
一个渲染对象直至如何布局及勾画自己及它的children。
RenderObject是Webkit的渲染对象泛型,它的定义如下:
class RenderObject{virtual void layout();virtual void paint(PaintInfo);virtual void rect repaintRect();Node* node; //the DOM nodeRenderStyle* style; // the computed styleRenderLayer* containgLayer; //the containing z-index layer}
每位渲染对象用一个和该节点的css盒模型相对应的圆形区域来表示,正如css2所描述的那样,它包含例如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display式样属性的影响(参考款式估算章节)。下边的webkit代码说明了怎样按照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中,假如一个元素想创建一个特殊的渲染对象,它须要复写“createRenderer”方法,使渲染对象指向不包含几何信息的款式对象。
渲染树和Dom树的关系TherendertreerelationtotheDOMtree
渲染对象和Dom元素相对应,但这些对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,比如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出现在渲染树中)。
还有一些Dom元素对应几个可见对象,它们通常是一些具有复杂结构的元素,难以用一个圆形来描述。诸如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按键。同样,当文本由于长度不够而折行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的反例是不规范的html,按照css规范,一个行内元素只能仅包含行内元素或仅包含片状元素,在存在混和内容时,将会创建匿名的球状渲染对象包裹住行内元素。
一些渲染对象和所对应的Dom节点不在树上相同的位置,比如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标示出它们原先的位置。
图12:渲染树及对应的Dom树
创建树的流程Theflowofconstructingthetree
Firefox中,叙述为一个窃听Dom更新的窃听器,将frame的创建指派给FrameConstructor,这个建立器估算款式(参看款式估算)并创建一个frame。
Webkit中,估算款式并生成渲染对象的过程称为attachment,每位Dom节点有一个attach方式,attachment的过程是同步的,调用新节点的attach方式将节点插入到Dom树中。
处理html和body标签将建立渲染树的根,这个根渲染对象对应被css规范称为containingblock的元素——包含了其他所有块元素的顶尖块元素。它的大小就是viewport——浏览器窗口的显示区域,Firefox称它为viewPortFrame,webkit称为RenderView,这个就是文档所指向的渲染对象,树中其他的部份都将作为一个插入的Dom节点被创建。
款式估算StyleComputation
创建渲染树须要估算出每位渲染对象的可视属性,这可以通过估算每位元素的款式属性得到。
款式包括各类来源的款式表,行内款式元素及html中的可视化属性(比如bgcolor),可视化属性转化为css款式属性。
款式表来始于浏览器默认款式表,及页面作者和用户提供的款式表——有些款式是浏览器用户提供的(浏览器容许用户定义喜欢的款式,比如,在Firefox中,可以通过在FirefoxProfile目录下放置款式表实现)。
估算款式的一些困难:
1.款式数据是十分大的结构,保存大量的款式属性会带来显存问题
2.假如不进行优化,找到每位元素匹配的规则会造成性能问题,为每位元素查找匹配的规则都须要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程倘若顺着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。
比如,下边这个复杂选择符
divdivdivdiv{…}
这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检测,这可能须要遍历节点树,最后却发觉它只是两个div的后代,并不使用该规则,之后则须要顺着另一条路径去尝试
3.应用规则涉及十分复杂的级联,它们定义了规则的层次
我们来看一下浏览器怎样处理这种问题:
共享式样数据
webkit节点引用式样对象(渲染款式),个别情况下,那些对象可以被节点间共享,这种节点须要是兄弟或是表兄弟节点,但是:
Firefox规则树Firefoxruletree
Firefox用两个树拿来简化式样估算-规则树和款式上下文树,webkit也有款式对象,但它们并没有储存在类似款式上下文树这样的树中,只是由Dom节点指向其相关的款式。
图14:Firefox款式上下文树
款式上下文包含最终值,这种值是通过以正确次序应用所有匹配的规则,并将它们由逻辑值转换为具体的值,比如,假如逻辑值为屏幕的比率,则通过估算将其转化为绝对单位。款式树的使用确实很巧妙,它促使在节点中共享的这种值不须要被多次估算,同时也节约了储存空间。
所有匹配的规则都储存在规则树中,一条路径中的底层节点拥有最高的优先级,这棵树包含了所找到的所有规则匹配的路径(今译:可以取巧理解为每条路径对应一个节点,路径上包含了该节点所匹配的所有规则)。规则树并不是一开始就为所有节点进行估算,而是在某个节点须要估算款式时,才进行相应的估算并将估算后的路径添加到树中。
我们将树上的路径看成词典中的词组,如果早已估算出了如下的规则树:
如果须要为内容树中的另一个节点匹配规则,如今晓得匹配的规则(以正确的次序)为B-E-I,由于我们早已估算出了路径A-B-E-I-L,所以树上早已存在了这条路径,剩下的工作就极少了。
现今来看一下树怎么保存。
结构化
式样上下文按结构界定,这种结构包括类似border或color这样的特定分类的款式信息。一个结构中的所有特点不是承继的就是非承继的,对承继的特点,除非元素自身有定义,否则就从它的parent承继。非承继的特点(称为reset特点)假如没有定义,则使用默认的值。
款式上下文树缓存完整的结构(包括估算后的值),这样,假如底层节点没有为一个结构提供定义,则使用下层节点缓存的结构。
使用规则树估算款式上下文
当为一个特定的元素估算款式时,首先估算出规则树中的一条路径,或是使用早已存在的一条,之后使用路径中的规则去填充新的款式上下文,从款式的底层节点开始,它具有最高优先级(一般是最特定的选择器),遍历规则树,直至塞满结构。假如在哪个规则节点没有定义所需的结构规则,则顺着路径向上,直至找到该结构规则。
假如最终没有找到该结构的任何规则定义,这么假如这个结构是承继型的,则找到其在内容树中的parent的结构,这些情况下,我们也成功的共享了结构;假如这个结构是reset型的,则使用默认的值。
假如特定的节点添加了值,这么须要做一些额外的估算以将其转换为实际值,之后在树上的节点缓存该值,使它的children可以使用。
当一个元素和它的一个兄弟元素指向同一个树节点时,完整的款式上下文可以被它们共享。
来看一个反例:假定有下边这段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 errorspan>errorp> div> <div class=”err” id=”div2″>another errordiv> body> html>
以及下边这种规则
div{margin:5px;color:black}.err{color:red}.big{margin-top:3px}divspan{margin-bottom:4px}#div1{color:blue}#div2{color:green}
简化下问题,我们只填充两个结构——color和margin,color结构只包含一个成员-颜色,margin结构包含四边。
生成的规则树如下(节点名:指向的规则)
上下文树如下(节点名:指向的规则节点)
假定我们解析html,遇见第二个div标签,我们须要为这个节点创建款式上下文,并填充它的款式结构。
我们进行规则匹配,找到这个div匹配的规则为1、2、6,我们发觉规则树上早已存在了一条我们可以使用的路径1、2,我们只需为规则6新增一个节点添加到下边(就是规则树中的F)。
之后创建一个款式上下文并将其放在上下文树中,新的款式上下文将指向规则树中的节点F。
如今我们须要填充这个款式上下文,先从填充margin结构开始,既然最后一个规则节点没有添加margin结构,顺着路径向上,直至找到缓存的后面插入节点估算出的结构,我们发觉B是近来的指定margin值的节点。由于早已有了color结构的定义浏览器工作原理是怎样的浏览器工作原理是怎样的,所以不能使用缓存的结构,既然color只有一个属性,也就不须要顺着路径向上填充其他属性。估算出最终值(将字符串转换为RGB等),并缓存估算后的结构。
第二个span元素更简单,进行规则匹配后发觉它指向规则G,和前一个span一样,既然有兄弟节点指向同一个节点,就可以共享完整的款式上下文,只需指向前一个span的上下文。
由于结构中包含承继自parent的规则,上下文树做了缓存(color特点是承继来的,但Firefox将其视为reset并在规则树中缓存)。
比如,假如我们为一个paragraph的文字添加规则:
p{font-family:Verdana;fontsize:10px;font-weight:bold}
这么这个p在内容树中的子节点div,会共享和它parent一样的font结构,这些情况发生在没有为这个div指定font规则时。
Webkit中,并没有规则树,匹配的申明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这种属性,是由于其他的依赖于它们-例如display),其次是高优先级important的,接着是通常优先级非important的,最后是通常优先级important的规则。这样,出现多次的属性将被根据正确的级联次序进行处理,最后一个生效。
总结一下,共享式样对象(结构中完整或部份内容)解决了问题1和3,Firefox的规则树帮助以正确的次序应用规则。
对规则进行处理以简化匹配过程