三. 浏览器内部工作原理-解析方向(二)
树的构建流程
1.3 Render Tree
1.3.1 渲染树的定义
当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。
Firefox将渲染树中的元素称为frames,WebKit则用renderer或渲染对象来描述这些元素.一个渲染对象知道怎么布局及绘制自己及它的 children?
RenderObject是Webkit的渲染对象基类,它的定义如下:
1 | class RenderObject{ |
每个渲染节点对象用一个和该节点 CSS 盒模型相对应的矩形区域来表示,正如 CSS2 所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的 display 样式属性的影响(参考样式计算章节)。下面的 WebKit 代码说明了如何根据 display 属性决定某个节点创建何种类型的渲染对象。
1 | RenderObject* RenderObject::createObject(Node* node, RenderStyle* style) { |
元素的类型也需要考虑,例如,表单控件和表格带有特殊的框架。
在Webkit中,如果一个元素想创建一个特殊的渲染对象,它需要重写“createRenderer”方法,使渲染对象指向不包含几何信息的样式对象。
简单总结:渲染树是可视节点的集合,即在页面真实渲染的元素集合,底层使用c++进行渲染,通过c++定义不同的对象实现不同类型节点的渲染
1.3.2 渲染树和DOM树的关系
渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,如head元素,display属性为none的元素(渲染树中存在 visibility 属性为 hidden 或着 opacity 属性为0的元素)。
还有一些Dom元素对应几个可见对象,它们一般是具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而换行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的 HTML结构,根据 CSS 规范,一个行内元素只能仅包含行内元素或仅包含块状元素,存在混合内容时,将会创建匿名的块状渲染对象包裹行内元素。
1.3.3 创建树的流程(The flow of constructing the tree)
Firefox中,表述为一个监听Dom更新的监听器,将frame的创建委派给Frame Constructor,这个构建器计算样式(参看样式计算)并创建一个frame。
Webkit中,计算样式并生成渲染对象的过程称为 attachment,每个Dom节点有一个attach方法,attachment的过程是同步的,调用新节点的 attach 方法将节点插入到Dom树中。处理 html 和 body 标签将构建渲染树的根,这个根渲染对象对应被 css 规范称为 containing block 的元素——包含了其他所有块元素的顶级块元素。它的大小就是 viewport——浏览器窗口的显示区域,Firefox称它为 viewPortFrame,webkit称为 RenderView,这个就是文档 document 所指向的渲染对象,树中其他的部分都将作为一个插入的Dom节点被创建。
简单总结:不同的内核对于合并的过程有不同的称呼,WebKit中使用attachment,构建渲染树会扫描DOM树和CSSOM树,扫描DOM树发现HTML、BODY元素时,就会创建一个根渲染对象,称为containing block,大小就是viewport。
1.3.4 样式计算(Style Computation)
创建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。
样式包括各种来源的样式表,行内样式元素及html中的可视化属性(例如bgcolor),可视化属性转化为css样式属性。
样式表的来源包括浏览器默认样式表、页面作者以及用户提供的样式表(有些样式是浏览器用户提供的(浏览器允许用户定义喜欢的样式,例如,在Firefox中可以通过在Firefox Profile目录下放置样式表实现))。
计算样式的一些困难:
- 样式数据是非常大的结构,保存大量的样式属性会带来内存问题。
- 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个过程有很大的工作量。选择符可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来却被证明是无用的路径,则必须去尝试另一条路径。例如,复杂选择符
div div div div{…}
这意味着规则应用到三个div的后代div元素,选择树某一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后代,并不使用该规则,然后则需要沿着另一条路径去尝试 - 应用规则涉及非常复杂的级联,它们定义了规则的层次
浏览器如何处理这些问题?
共享样式数据(Sharing style data)
WebKit节点引用样式对象(渲染样式),某些情况下,这些对象可以被节点间共享,这些节点需要是兄弟或是表兄弟节点,并且:
- 这些元素必须处于相同的鼠标状态(比如不能一个处于hover,而另一个不是)
- 不能有元素具有id
- 标签名必须匹配
- class属性必须匹配
- 对应的属性必须相同
- 链接状态必须匹配
- 焦点状态必须匹配
- 不能有元素被属性选择器影响
- 元素不能有行内样式属性
- 不能有生效的兄弟选择器,webcore在任何兄弟选择器相遇时只是简单的抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child和:last-child这样的选择器。
Firefox规则树(Firefox rule tree)
Firefox用两个树用来简化样式计算-规则树和样式上下文树,WebKit也有样式对象,但它们并没有存储在类似样式上下文树这样的树中,只是由Dom节点指向其相关的样式。
具体细节可以阅读原文:浏览器内部工作原理_知识库_博客园 (cnblogs.com)
Webkit中,并没有规则树,匹配的声明会被遍历四次,先是应用非important的高优先级属性(之所以先应用这些属性,是因为其他的依赖于它们-比如display),其次是高优先级important的,接着是一般优先级非important的,最后是一般优先级important的规则。这样,出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。
1.4 Layout
当渲染对象被创建并添加到树中,它们仅具有对应的布局规则,并没有实际的位置和大小,计算这些值的过程称为 layout 或 reflow。
Html使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左、自上而下的进行。也存在一些例外,比如 html tables。
坐标系统相对于根frame,使用 top 和 left 坐标。布局是一个递归的过程,由根渲染对象开始,它对应 html 文档document元素,布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。根渲染对象的位置是 0,0,它的大小是viewport-浏览器窗口的可见部分。所有的渲染对象都有一个 layout 或 reflow 方法,每个渲染对象调用需要布局的 children 的 layout 方法。
简单总结:通过父元素掉调用需要布局的 children 的 layout 方法,如何明确元素需要布局,则需要使用到 Dirty bit 系统
1.4.1 Dirty bit系统
为了不因为每个小变化都全部重新布局,浏览器使用一个 dirty bit 系统,一个渲染对象发生了变化或是被添加了,就标记它及它的children为dirty——需要layout。
dirty bit 设置元素存在两个标识——dirty及children are dirty,children are dirty标识说明即使这个渲染对象本身的属性没变化,它也至少有一个child需要layout。
1.4.2 全局和增量layout
当layout在整棵渲染树触发时,称为全局layout,这可能在下面这些情况下发生:
- 一个全局的样式改变影响所有的渲染对象,比如字号的改变。
- 窗口resize。
layout也可以是增量的,这样只有标志为dirty的渲染对象会重新布局(也将导致一些额外的布局)。增量layout会在渲染对象dirty时异步触发,例如,当网络接收到新的内容并添加到DOM树后,新的渲染对象会添加到渲染树中,即在初次渲染时,是一个增量layout的过程。
异步和同步layout
增量layout的过程是异步的,Firefox为增量 layout 生成了 reflow 队列,以及一个调度执行这些批处理命令。WebKit也有一个计时器用来执行增量layout-遍历树,为dirty状态的渲染对象重新布局。另外,当脚本请求样式信息时,例如“offsetHeight”,会同步的触发增量布局。
全局的layout一般都是同步触发。有些时候,layout会被作为一个初始layout之后的回调,比如滑动条的滑动。
优化
当一个layout因为resize或是渲染位置改变(并不是大小改变)而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。
一般情况下,如果只有子树发生改变,则layout并不从根开始。这种情况发生在,变化发生在元素自身并且不影响它周围元素,例如,将文本插入文本域(否则,每次击键都将触发从根开始的重排)。
1.4.3 layout过程
layout一般有下面这几个部分:
parent渲染对象决定它的宽度
parent渲染对象读取chilidren,并:
- 放置child渲染对象(设置它的x和y)
- 在需要时(它们当前为dirty或是处于全局layout或者其他原因)调用child渲染对象的layout,这将计算child的高度
- parent渲染对象使用child渲染对象的累积高度,以及margin和padding的高度来设置自己的高度-这将被parent渲染对象的parent使用
- 将dirty标识设置为false
Firefox使用一个“state”对象(nsHTMLReflowState)做为参数去布局(firefox称为reflow),state包含parent的宽度及其他内容。
Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它包括渲染对象计算出的高度。
宽度计算
渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度及margin、border进行计算。例如,下面这个div的宽度:
webkit中宽度的计算过程是(RenderBox类的 calcWidth 方法):
- 容器的宽度是容器的可用宽度和0中的最大值,这里的可用宽度为:contentWidth=clientWidth()-paddingLeft()-paddingRight(),clientWidth和clientHeight代表一个对象内部的不包括border和滑动条的大小
- 元素的宽度指样式属性width的值,它可以通过计算容器的百分比得到一个绝对值
- 加上水平方向上的border和padding
到这里是最佳宽度的计算过程,现在计算宽度的最大值和最小值,如果最佳宽度大于最大宽度则使用最大宽度,如果小于最小宽度则使用最小宽度。最后缓存这个值,当需要layout但宽度未改变时使用。
Line breaking
当一个渲染对象在布局过程中需要折行时,则暂停并告诉它的parent它需要折行,parent将创建额外的渲染对象并调用它们的layout。
1.5 绘制(Painting)
绘制阶段,遍历渲染树并调用渲染对象的paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件,这在UI的章节有更多的介绍。
1.5.1 全局和增量
和布局一样,绘制也可以是全局的——绘制完整的树,或增量的。在增量的绘制过程中,一些渲染对象以不影响整棵树的方式改变,改变的渲染对象使其在屏幕上的矩形区域失效,这将导致操作系统将其看作dirty区域,并产生一个paint事件,操作系统很巧妙的处理这个过程,并将多个区域合并为一个,提高性能。Chrome中,这个过程更复杂些,因为渲染对象在不同的进程中,而不是在主进程中。Chrome在一定程度上模拟操作系统的行为,表现为监听事件并派发消息给渲染根,在树中查找到相关的渲染对象,重绘这个对象(往往还包括它的children)。
1.5.2 绘制顺序
css2定义了绘制过程的顺序——http://www.w3.org/TR/CSS21/zindex.html。这个就是元素压入堆栈的顺序,这个顺序影响着绘制,堆栈从后向前进行绘制。
一个块渲染对象的堆栈顺序是:
- 背景色
- 背景图
- border
- children
- outline
1.5.3 Firefox显示列表
Firefox读取渲染树并为绘制的矩形创建一个显示列表,该列表以正确的绘制顺序包含这个矩形相关的渲染对象(背景、图片、样式属性等以正确的顺序排序)。用这样的方法,可以使重绘时只需查找一次树,而不需要多次查找——绘制所有的背景、所有的图片、所有的border等等。
Firefox优化了这个过程,它不添加会被隐藏的元素,比如元素完全在其他不透明元素下面。
1.5.4 WebKit矩形存储
重绘前,WebKit将旧的矩形保存为位图,然后只绘制新旧矩形的差集。
1.5.5 动态变化
浏览器总是试着以最小的动作响应一个变化,所以一个元素颜色的变化将只导致该元素的重绘,元素位置的变化将大致元素的布局和重绘,添加一个Dom节点,也会导致这个元素的布局和重绘。一些主要的变化,比如增加html元素的字号,将会导致缓存失效,从而引起整体的布局和重绘。
1.6 渲染引擎的线程
渲染引擎是单线程的,除了网络操作以外,几乎所有的事情都在单一的线程中处理,在Firefox和Safari中,这是浏览器的主线程,Chrome中这是tab的主线程(Chrome一个Tab一个进程,一个进程中含有一个主线程,负责大部分的代码执行)。
网络操作由几个并行线程执行,并行连接的个数是受限的(通常是2-6个)。
1.7 事件循环
浏览器主线程是一个事件循环,它被设计为无限循环以保持执行过程的可用,等待事件(例如layout和paint事件)并执行它们,注意并不是无限制的轮询,而是一个带有阻塞的轮询。下面是Firefox的主要事件循环代码
1 | while (!mExiting) |
1.8 CSS2可视模型(CSS2 visual module)
1.8.1 画布The Canvas
根据CSS2规范,术语canvas用来描述格式化的结构所渲染的空间——浏览器绘制内容的地方。画布对每个维度空间都是无限大的,但浏览器基于viewport的大小选择了一个初始宽度。根据http://www.w3.org/TR/CSS2/zindex.html的定义,画布如果是包含在其他画布内则是透明的,否则浏览器会指定一个颜色。
1.8.2 CSS盒模型
CSS盒模型描述了矩形盒,这些矩形盒是为文档树中的元素生成的,并根据可视的格式化模型进行布局。每个box包括内容区域(如图片、文本等)及可选的四周padding、border和margin区域。
每个节点生成0-n个这样的box。所有的元素都有一个display属性,用来决定它们生成box的类型,例如:
- block -生成块状box
- inline -生成一个或多个行内box
- none -不生成box
默认的是inline,但浏览器样式表设置了其他默认值,例如,div元素默认为block。可以访问http://www.w3.org/TR/CSS2/sample.html查看更多的默认样式表示例。
1.8.3 定位策略Position scheme
这里有三种策略:
- normal -对象根据它在文档的中位置定位,这意味着它在渲染树和在Dom树中位置一致,并根据它的盒模型和大小进行布局。
- float -对象先像普通流一样布局,然后尽可能的向左或是向右移动。
- absolute -对象在渲染树中的位置和Dom树中位置无关。
static 和 relative 是normal, float属于float,absolute和fixed属于absolute。
在static定位中,不定义位置而使用默认的位置。其他策略中,作者指定位置——top、bottom、left、right。
1.8.4 Box布局
Box布局的方式由这几项决定:box的类型、box的大小、定位策略及扩展信息(比如图片大小和屏幕尺寸)。
Box类型
- Block box:构成一个块,即在浏览器窗口上有自己的矩形
- Inline box:并没有自己的块状区域,但包含在一个块状区域内
区分
- block一个挨着一个垂直格式化,inline则在水平方向上格式化。
- Inline盒模型放置在行内或是line box中,每行至少和最高的box一样高,当box以baseline对齐时——即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的box高。当容器宽度不够时,行内元素将被放到多行中,这在一个p元素中经常发生。
1.8.5 定位Position
Relative
相对定位——先按照一般的定位,然后按所要求的差值移动。
Floats
一个浮动的box移动到一行的最左边或是最右边,其余的box围绕在它周围。下面这段html:
1 | <p> |
Absolute和Fixed
这种情况下的布局完全不顾普通的文档流,元素不属于文档流的一部分,大小取决于容器。Fixed时,容器为viewport(可视区域)。
Layered representation
这个由CSS属性中的z-index指定,表示盒模型的第三个大小,即在z轴上的位置。Box分发到堆栈中(称为堆栈上下文),每个堆栈中靠后的元素将被较早绘制,栈顶靠前的元素离用户最近,当发生交叠时,将隐藏靠后的元素。堆栈根据z-index属性排序,拥有z-index属性的box形成了一个局部堆栈,viewport有外部堆栈
1 | <STYLE type="text/css"> |
虽然绿色div排在红色div后面,可能在正常流中也已经被绘制在后面,但z-index有更高优先级,所以在根box的堆栈中更靠前。