浏览器是如何工作的系列:渲染树的构建

四、渲染树的构建
	当DOM被构建时,浏览器构建另一个渲染树,这棵树上的可视化元素将被按正确的顺序显示。这是文档的可视化表示。
	目的在于使可视化元素按他们的顺序渲染他们的内容。
	在Firefox中,这些元素在渲染树中称作“帧”,webkit使用术语渲染器或者渲染对象,一个渲染器知道如何去布局和绘制它自己和它的孩子。
	Webkits 渲染对象类,基于渲染器的类有以下定义:
	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.0规则,它包含了几何信息,如宽、高和位置。
	盒子的类型受与节点相关的“display”样式属性影响,下面是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;
	}
	考虑到元素类型,例如表单控件和表格有特殊的帧。Chrome浏览器中input表单控件,浏览器Computed Style中显示display:inline-block; table->display:table
	thead->display: table-header-group;tr->display: table-row;tbody->display: table-row-group;th/td->display: table-cell;
	在webkit中,如果一个元素想创建一个特殊的渲染器,它将重写“createRender”方法,渲染器指向包含非几何信息的样式对象。
	
	1.渲染树关联到DOM树
		渲染器和DOM元素相对应,但是不是一一对应关系,不可见的元素将不会插入到渲染树,
		例如“head”元素,该元素的显示属性会设置为“none”,将不会显示到渲染树,(设置visibility:hidden的元素会出现在DOM树)。
		有些DOM元素会对应几个可见对象(渲染器),通常这些元素有着复杂的结构,不能被描述成单一的矩形,
		例如“select”元素
		有三个渲染器——一个显示area,一个下拉框,一个下拉按钮。此外,当文本被分隔成多行时,因为宽度不足显示在一行,
		那个新行将作为一个额外的渲染器被添加。
		另一个例子,好几个渲染器打破了HTML。根据CSS规范,一个行内元素必须包含唯一的块元素或者仅内联元素,在混合内容的情况下
		匿名块渲染器将会被创建并包裹那个内联元素。
		
		一些渲染器对象和一个DOM节点对应,但是不是在树中的同一个地方,浮动和绝对定位的元素是脱离了流,被放置到渲染树
		不同的地方,并映射到真实的帧。应该是一个占位符框架。
		下图:4.1.1 渲染树和对应的DOM树


		
	2.流构造树
	在Firefox中,这个介绍被注册为一个DOM更新监听器。介绍委派帧创建到“FrameConstructor”和构造解析样式(样式计算),并创建了一个框架。
	在Webkit,解释样式的过程和创建一个渲染器被称为“attachment”.每一个DOM节点有一个“attach”方法,attachment是同步的,
	节点插入到DOM树会调用该新节点的“attach”方法。
	
	在渲染树根节点,HTML和body 标签处理结果,根渲染队形对应到CSS规范中叫做包含块——包含所有的其他块的最顶端块。
	它的尺寸是视口-在浏览器窗口的显示区域尺寸。火狐称它为ViewPortFrame、Webkit称为RenderView。这是该文档节点的渲染对象。其余的构造树作为一个DOM节点插入。 
	关于这个主题,请参阅CSS2 - http://www.w3.org/TR/CSS21/intro.html处理模型
	3.样式计算
		构建渲染树需要去计算每个可见渲染对象的属性,即计算每个元素的样式属性。样式包括来自各种样式表、内联样式
		和可见属性在HTML标签中的,后来被翻译成匹配的CSS样式属性。
		样式表的起源是浏览器的默认的样式表,风格表的页面作者和用户样式表提供 - 这些样式表提供用户的浏览器
		(浏览器让你定义自己喜欢的风格。在Firefox中,例如,这是通过放置一个样式表在“火狐配置文件”文件夹)中。
		
		样式计算带来了一些困难:
			a)样式数据是一个非常大的结构,列举多种样式属性,这可能会导致内存问题。
			b)查找每个元素的匹配规则可能会导致性能问题,如果它不优化。遍历整个规则列表中的每个元素找到匹配的是一个沉重
			的任务。选择器可以有复杂的结构,可能会导致匹配过程开始于一个看似前途的道路被证明是徒劳的,必须尝试另一条路径。 
			例如-这种混合选择器:
			div div div div{
			...
			}
			这意味着规则应用到三个div的后代div元素,选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,
			并不使用该规则,然后则需要沿着另一条路径去尝试
			c)应用规则涉及到相当复杂的级联规则,规则定义的规则的层次。
		让我们来看看浏览器是如何面对这些问题:
		1)共享样式数据
			webkit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:
			这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)
			不能有元素具有id
			标签名必须匹配
			class属性必须匹配
			对应的属性必须相同
			链接状态必须匹配
			焦点状态必须匹配
			不能有元素被属性选择器影响
			元素不能有行内样式属性
			不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,
			并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。
		2)Firefox的规则树
			Firefox用两个树用来简化样式计算-规则树和样式上下文树,webkit也有样式对象,
			但它们并没有存储在类似样式上下文树这样的树中,只是由Dom节点指向其相关的样式。
			如下图:4.3.2 Firefox样式上下文


			样式上下文包含最终值,这些值是通过以正确顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值,
			例如,如果逻辑值为屏幕的百分比,则通过计算将其转化为绝对单位。样式树的使用确实很巧妙,
			它使得在节点中共享的这些值不需要被多次计算,同时也节省了存储空间。
			
			所有匹配的规则都存储在规则树中,一条路径中的底层节点拥有最高的优先级,这棵树包含了所找到的 
			所有规则匹配的路径(译注:可以取巧理解为每条路径对应一个节点,路径上包含了该节点所匹配的所有规则)。
			规则树并不是一开始就为所有节点进行计算,而是 在某个节点需要计算样式时,才进行相应的计算并将计算后的路径添加到树中。
			我们将树上的路径看成辞典中的单词,假如已经计算出了如下的规则树:如图4.3.2.1


			假如需要为内容树中的另一个节点匹配规则,现在知道匹配的规则(以正确的顺序)为B-E-I,因为我们已经计算出了路径A-B-E-I-L,
			所以树上已经存在了这条路径,剩下的工作就很少了。

			现在来看一下树如何保存。
			a)结构化
			样式上下文按结构划分,这些结构包括类似border或color这样的特定分类的样式信息。一个结构中的所有特性不是继承的就是非继承的,对继承的特性,
			除非元素自身有定义,否则就从它的parent继承。非继承的特性(称为reset特性)如果没有定义,则使用默认的值。
			样式上下文树缓存完整的结构(包括计算后的值),这样,如果底层节点没有为一个结构提供定义,则使用上层节点缓存的结构。
			b)使用规则树计算样式上下文
			当为一个特定的元素计算样式时,首先计算出规则树中的一条路径,或是使用已经存在的一条,然后使 用路径中的规则去填充新的样式上下文,
			从样式的底层节点开始,它具有最高优先级(通常是最特定的选择器),遍历规则树,直到填满结构。如果在那个规则节点 
			没有定义所需的结构规则,则沿着路径向上,直到找到该结构规则。

			如果最终没有找到该结构的任何规则定义,那么如果这个结构是继承型的,则找到其在内容树中的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  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.	#div2 {color:green}
			
			简化下问题,我们只填充两个结构——color和margin,color结构只包含一个成员-颜色,margin结构包含四边。
			如下图4.3.2.2,生成的规则树,.3.2.3 上下文树


			假设我们解析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;font size:10px;font-weight:bold}

			那么这个p在内容树中的子节点div,会共享和它parent一样的font结构,这种情况发生在没有为这个div指定font规则时。

			Webkit中,并没有规则树,匹配的声明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这些属性,
			是因为其他的依赖于它们-比如display),其次是高优先级important的,接着是一般优先级非important的,最后是一般优先级important的规则。
			这样,出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。

			总结一下,共享样式对象(结构中完整或部分内容)解决了问题1和3,Firefox的规则树帮助以正确的顺序应用规则。
		3)对规则进行处理以简化匹配过程
		样式规则有几个来源:
		· 外部样式表或style标签内的css规则
		p {color:blue}
		· 行内样式属性
		<p bgcolor="blue" />
		· html可视化属性(映射为相应的样式规则)
		后面两个很容易匹配到元素,因为它们所拥有的样式属性和html属性可以将元素作为key进行映射。
		就像前面问题2所提到的,css的规则匹配可能很狡猾,为了解决这个问题,可以先对规则进行处理,以使其更容易被访问。
		解析完样式表之后,规则会根据选择符添加一些hash映射,映射可以是根据id、class、标签名或是任何不属于这些分类的综合映射。
		如果选择符为id,规则将被添加到id映射,如果是class,则被添加到class映射,等等。
		这个处理是匹配规则更容易,不需要查看每个声明,我们能从映射中找到一个元素的相关规则,这个优化使在进行规则匹配时减少了95+%的工作量。
		来看下面的样式规则:
		p.error {color:red}
		#messageDiv {height:50px}
		div {margin:5px}
		第一条规则将被插入class映射,第二条插入id映射,第三条是标签映射。
		下面这个html片段:
		<p class="error">an error occurred </p>
		<div id=" messageDiv">this is a message</div>
		我们首先找到p元素对应的规则,class映射将包含一个“error”的key,找到p.error的规则,div在id映射和标签映射中都有相关的规则,
		剩下的工作就是找出这些由key对应的规则中哪些确实是正确匹配的。
		例如,如果div的规则是table div {margin:5px}
		这也是标签映射产生的,因为key是最右边的选择符,但它并不匹配这里的div元素,因为这里的div没有table祖先。
		Webkit和Firefox都会做这个处理。
		4)在正确的级联顺序应用规则
		样式对象拥有对应所有可见属性的属性,如果特性没有被任何匹配的规则所定义,那么一些特性可以从parent的样式对象中继承,另外一些使用默认值。
		这个问题的产生是因为存在不止一处的定义,这里用级联顺序解决这个问题。
			a)样式表的级联顺序
			一个样式属性的声明可能在几个样式表中出现,或是在一个样式表中出现多次,
			因此,应用规则的顺序至关重要,这个顺序就是级联顺序。根据css2的规范,级联顺序为(从低到高):
				1. 浏览器声明
				2. 用户声明
				3. 作者的一般声明
				4. 作者的important声明
				5. 用户important声明
				浏览器声明是最不重要的,用户只有在声明被标记为important时才会覆盖作者的声明。
				具有同等级别的声明将根据specifity以及它们被定义时的顺序进行排序。
				Html可视化属性将被转换为匹配的css声明,它们被视为最低优先级的作者规则。
			b)Specifity
			Css2规范中定义的选择符specifity如下:

			· 如果声明来自style属性,而不是一个选择器的规则,则计1,否则计0(=a)
			· 计算选择器中id属性的数量(=b)
			· 计算选择器中class及伪类的数量(=c)
			· 计算选择器中元素名及伪元素的数量(=d)
			连接a-b-c-d四个数量(用一个大基数的计算系统)将得到specifity。这里使用的基数由分类中最高的基数定义。
			例如,如果a为14,可以使用16进制。不同情况下,a为17时,则需要使用阿拉伯数字17作为基数,
			这种情况可能在这个选择符时发生html body div div …(选择符中有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 */
			c)排序规则
			规则匹配后,需要根据级联顺序对规则进行排序,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; 
			}
	4.循序渐进的过程
	webkit使用一个标志位标识所有顶层样式表都已加载,如果在attch时样式没有完全加载,
	则放置占位符,并在文档中标记,一旦样式表完成加载就重新进行计算。


 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值