学习记录整理

任何一个网站的页面展示离不开HTML、CSS、JavaScript,也正是这三者,构成了美丽的web页面,那么在浏览器中,是如何将3者完美地展示出来呢?这就是浏览器的渲染。

HTML、CSS 和 JavaScript 关系图

HTML: 是由标记和文本组成,是页面要显示的文字内容

CSS:又称为层叠样式,用于对html的内容进行更为丰富的描绘(在没有使用css之前就只能用html实现,但html支持不够多,且不够灵活)

Javascript:又称js,可以让html页面内容“动”起来,更为灵活地控制处理html、css。

在理解了三者的意义后,我们开始正题,理解浏览器渲染的详细过程。

渲染机制很复杂,在过程中会分为很多个子阶段,按照渲染的时间顺序可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、栅格化和合成。而每一个子阶段又包含:输入内容、处理过程、输出内容。

一、构建DOM树阶段(html是有父子层次结构的,有html\head头、有body内容)浏览器是无法直接理解html标记内容,需要转为DOM树之后,才能正常输出显示

DOM树结构跟html代码层次结构显示上是一样的,不一样的地方是DOM树是存在内存里面,可以由js动态修改。(可以使用“开发者工具”,选择“Console”标签来打开控制台,在控制台里面输入“document”后回车,就能看到一个完整的 DOM 树结构)

二、样式计算阶段(生成dom树之后,只能确认html内容显示顺序/位置,还要进行计算样式才能描绘得更好看),具体会做以下3个处理

A、获取样式来源:

  • 通过 link 引用的外部 CSS 文件
  • style标记内的 CSS
  • 元素的 style 属性内嵌的 CSS

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets(同样可以使用开发者的Console工具,输入document.styleSheets回车后查看styleSheets结构)

document.styleSheets

从图中能看到很多种样式,且三种来源都有(styleSheets结构不是这章的重点)只需要知道渲染引擎会把获取的CSS文本全转为styleSheets结构中的数据,跟DOM树一样,可供js修改样式提供了基础。

B、转换样式表中的属性值,使其标准化(css转变成可以理解的结构后,就需要对属性值进行转换了,先看一段css样式)


body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }

2em、blue、bold人类容易理解、但渲染引擎不容易理解(就像ip与域名一样),所以需要将所有值转换为渲染引擎更容易理解、能进行运算的具体数值。

css属性值转换示例

C、计算出DOM树中每个节点的具体样式(能进行css属性具体数值后,就要对dom树一一赋样式值了)这就涉及到了CSS《继承规则》《层叠规则》。

继承规则:每个DOM节点都包含父节点的样式。

如图所示,所有子节点都继承了父节点Body的font-size属性

可以通过开发者工具的element的style标签,加深对继承的理解

  • 首先,可以选择要查看的元素的样式(位于图中的区域 2 中),在图中的第 1 个区域中点击对应的元素元素,就可以了下面的区域查看该元素的样式了。比如这里我们选择的元素是p标签,位于html.body.div这个路径下面。
  • 其次,可以从样式来源(位于图中的区域 3 中)中查看样式的具体来源信息,看看是来源于样式文件,还是来源于 UserAgent 样式表。这里需要特别提下 UserAgent 样式,它是浏览器提供的一组默认样式,如果你不提供任何样式,默认使用的就是 UserAgent 样式。
  • 最后,可以通过区域 2 和区域 3 来查看样式继承的具体过程。

层叠规则:css的基本特征,定义了如何合并多个来源的属性值算法,也是css的核心,css全称“层叠样式表”正是强调了这一点(此次不多介绍,自行搜索)

总之,样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
浏览器查看最终样式位置
三、布局阶段(有了DOM树与树元素的样式还不足以显示页面,还需要计算出DOM树元素的几何位置信息)这一个计算过程称为布局

Chrome在此阶段有两个任务:创建布局树与布局计算。创建布局树简单点说就是把DOM树与ComputedStyle(样式计算结果)合成一个新的结果集(即布局树)

如图所示,DOM 树中只有部分节点在布局树中,html下的head及子节点,body下的span都不存在布局树中

在创建布局树的工作如下:

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
  • 不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。

布局计算:就是计算出HTML标记的具体位置信息,计算过程复杂,放在后面再说明(目前:在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容)

四、分层(有了布局树,且具体位置信息也计算出来了,还是未能绘制页面,还要进行DOM树节点分层)

页面中有很多复杂的效果(3D变换、页面滚动、用z-indexing做z轴排序等 )为了实现这些效果,渲染引擎还特定的节点生成专用的图层,生成一个对应的图层树(Layer Tree)这就跟ps中的层是一样的概念,多个图层叠加一起构成了最终的一个图片

使用“开发者工具”的Layers能看到页面分层情况
布局树与图层树关系图
  • 通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如上图中的 span 标签没有专属图层,那么它们就从属于它们的父节点图层。但不管怎样,最终每一个节点都会直接或者间接地从属于一个层。

渲染引擎是如何创建新层呢?有以下任意一种情况都会创建新层。

第一点:拥有层叠上下文属性的元素(position、z-index、filter、opacity…)会被提升为单独的一层,更多层叠上下文可见参考

层叠上下文示意图

第二点:需要剪裁(clip)的地方也会被创建为图层(如下代码与说明图)


<style>
      div {
            width: 200;
            height: 210;
            padding:10px;
            border:1px solid #ff0000;
        } 
</style>
<body>
    <div >
        <p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
        <p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
        <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> 
    </div>
</body>
内容超过div层的指定大小
  • 出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:
五、图层绘制(构建完图层树之后,就要对每一层图层树进行绘制)

渲染引擎会像画图一样,将绘制拆分与很多小的绘制指令,组成待绘制序列表

绘制列表
  • 绘制列表中的指令其实就是执行一个非常简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
“开发者工具”的“Layers”标签(区域 1 就是 document 的绘制列表,拖动区域 2 中的进度条可以重现列表的绘制过程)

绘制列表只是记录指令的列表,实际上操作是由渲染引擎中《合成线程》来完成,当绘制列表准备好之后,主线各会把绘制列表提交(commit)给《合成线程》 来处理

六、栅格化(raster)操作

一个网页可能会很长,不只是电脑当前屏幕这一点内容,上下滚动还会有很大部分的内容,一般把浏览器当前可见的位置称为视口(如首屏/屏幕显示内容)通过视口,用户只会看到页面很小的一部分,所以在绘制时候,不会绘制所有的html内容,而是由《合成线程》将图层划分为图块(tile)。

合成线程会按照视口位置,优先将《可见图块》生成位图,而生成位图的操作是由栅格化来执行的,所谓栅格化,就是将图块转换为位图。

渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的
  • 通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
  • 渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中
七、合成与显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程的viz组件会接收《合成线程》的DrawQuad命令,然后将其页面内容绘制到内在中,最后将内存数据显示到屏幕上。

完整的渲染流水线示意图
  • DOM:渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  • Style:渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  • Layout:创建布局树,并计算元素的布局信息。
  • Layer:对布局树进行分层,并生成分层树
  • Paint:为每个图层生成绘制列表,并将其提交到合成线程。
  • tiles-raster:合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图
  • Drawquad:合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  • Display:浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
整体渲染流水完成后,再补充一下几个概念“重排”、“重绘”、“合成”

重排:通过js或css修改位置属性(width、height、padding…)就会触发重新布局,《Style-样式计算》后续子阶段都要重走,这是要更新完整的渲染流水线,开销最大

重绘:通过js或css修改背景颜色(backgroud、border…)就会触发html元素的重新绘制,《Paint-图层绘制》子阶段都要重走

合成:如果你更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的《tiles合成》操作

重排:流程示意图
重绘:流程示意图
合成:流程示意图