我之前一直有个疑问,为什么编写的 HTML、CSS、JavaScript
等文件,经过浏览器运行之后就会显示出我们想要的页面呢?他们是如何转化为页面的?这背后的原理是什么??经过一番折腾,终于找到了答案,也想跟大家分享一下~
其实,这个过程是浏览器的渲染进程来实现的。渲染器进程的核心工作是将 HTML、CSS 和 JavaScript
转换为用户可以交互的网页。
由于渲染机制比较复杂,所以渲染模块在执行过程中会被划分为很多子阶段 ,输入的静态资源经过这些子阶段,最后输出页面。我们将一个处理流程称为渲染流水线,其大致流程如下图所示:
总的来说,可分为这几个阶段 DOM树构建
、CSSOM树构建
、渲染树构建
、页面布局
、页面绘制
下面就分别来看看这些过程都做了哪些操作
一、DOM树构建
可能会有小伙伴会问为什么要构建DOM树呢? 因为浏览器是无法直接理解和使用 HTML ,所以需要将 HTML 转化为浏览器能够理解的结构,也就是 DOM 树。
在了解 DOM 树构建之前,我们先来看看,什么是树🌲,下面我画了 2 幅图,来方便大家理解
相信大家都能一眼看出来,只有第一个是 树 结构,树是由结点或顶点和边组成的且不存在着任何环的一种数据结构 ,而右边的图,很明显存在了一个闭环,所以它不是一个 树 结构
回归正题,那么什么是 DOM 树 呢? 在页面里,每个 HTML标签
都会被浏览器解析成一个个文档对象。HTML
本质上就是一个嵌套结构,在解析的时候,就会把每一个文档对象用一个树形结构组织起来,最后挂在document上,这种组织方式就是 HTML
最基础的结构------ 文档对象模型(DOM) ,这棵树上的文档对象就叫做DOM节点,DOM 是浏览器的页面内部表示,也是 Web 开发人员可以通过 JavaScript 进行交互的数据结构和 API。
相信大家都了解什么是 DOM 树啦,接下来,我们来看下它是怎么构建的:
其主要过程,可以通过这幅图来描述:
HTML解析器
将 HTML
字节流转换为 DOM
结构,其转化过程如下:
1. 字符流 → 词(token)
HTML结构会首先通过分词器
将字节流拆分为词(token)
。Token分为Tag Token
和文本 Token
。 例子:
css
<p>hello world</p>
可以看到,Tag Token 又分 StartTag 和 EndTag, <p>
就是 StartTag , </p>
就是 EndTag,分别对应图中的蓝色和红色块,文本 Token 对应绿色块。
2. 词(token)→ DOM树
接下来就需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。这个过程是通过栈结构来实现的,这个栈主要用来计算节点之间的父子关系,上面步骤中生成的token会按顺序压入栈中
下面来看看这的 Token 栈是如何工作的,
例子:
开始时, HTML 解析器会创建一个根为 document
的空的 DOM
结构,同时将 StartTag document
的 Token 压入栈
中,然后再将解析出来的第一个 StartTag html
压入栈中,并创建一个 html
的 DOM 节点,添加到 document 上,这时 Token 栈和 DOM 树如下:
接下来 body
和 div
标签也会和上面的过程一样,进行入栈操作:
随后就会解析到 div 标签中的文本Token
,渲染引擎会为该 Token 创建一个文本节点
,并将该 Token 添加到 DOM 中,它的父节点就是当前 Token 栈顶元素对应的节点:
接下来就是第一个EndTag div
,这时 HTML 解析器会判断当前栈顶元素是否是 StartTag div
,如果是,则从栈顶弹出 StartTag div,如下图所示: 依次不断进栈出栈,最终的结果如下:
那么 DOM 树
就构建完毕啦!❀❀❀
二、CSSOM树构建
同样,浏览器也是无法直接理解CSS
代码的,需要将其解析成浏览器可以理解的CSSOM树
。实际上。浏览器在构建 DOM 树的同时,如果样式也加载完成了,那么 CSSOM 树也会同步构建。
📢 在将CSS
转化为树形对象
之前,还需要将样式表里所有值
转化为浏览器渲染引擎容易理解的、标准化的计算值
,这个过程就是属性值标准化。比如,当遇到以下CSS样式:
less
body { font-size: 2em,font-weight: bold,color:green; }
经过标准化的过程,上面的代码会变成这样:
less
body { font-size: 32px,font-weight: 700,color:rgb(0, 128, 0); }
样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程 中需要遵守 CSS 的继承
和层叠
两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。
例子:
js
<head>
<link href="./style.css">
<style>
.box {
width: 100px;
height: 50px;
background: red;
}
.content {
font-size: 25px;
line-height: 25px;
margin: 10px;
}
</style>
</head>
<body>
<div class="box">
<div>hello</div>
</div>
<p style="color: blue" class="content"> <span>hello world</span>
<p style="display: none;">浏览器</p>
</p>
</body>
</html>
最终构建的 CSSOM 树
大致如下:
三、渲染树构建
在 DOM 树
和 CSSOM 树
都渲染完成之后,就会进入渲染树
的构建阶段。渲染树就是 DOM 树和 CSSOM 树的结合,会得到一个可以知道每个节点会应用什么样式的数据结构。
构建出的渲染树大致如下:
需要注意的是,DOM树中不可见的节点不会包含到渲染树中。为了构建渲染树,浏览器上大致做了如下工作:遍历DOM树中所有可见节点,并把这些节点加到布局中,而不可见的节点会被布局树忽略掉,如 head
标签下面的全部内容,再比如 p.p
这个元素,因为它的属性包含 dispaly:none
,所以这个元素也没有被包含进渲染树中。如果给元素设置了visibility: hidden;
属性,那这个元素会出现在渲染树中,因为具有这个样式的元素是需要占位的,只不过不需要显示出来。
四、页面布局
经过上面的步骤,一棵渲染树就生成啦,这棵树就是展示页面的关键。到现在为止,需要渲染的所有节点之间的结构关系及其样式信息就确定好了。接下来就进入页面的布局过程。
通过计算渲染树上每个节点的样式,能得出来每个元素所占空间的大小和位置。当有了所有元素的大小和位置后,就可以在浏览器的页面区域里去绘制元素的边框了。那么,这个过程就是布局 。这个过程中,浏览器对渲染树进行遍历,将元素间嵌套关系以盒模型
的形式写入文档流:
盒模型在布局过程中会计算出元素确切的大小和定位。计算完毕后,相应的信息被写回渲染树上,就形成了布局渲染树
五、页面绘制
1. 构建图层
经过布局,每个元素的位置和大小就有了,那下面是不是就该开始绘制页面了?答案是否定的,因为页面上可能有很多复杂的场景,比如3D变化
、页面滚动
、使用z-index
进行z轴的排序等。所以,为了实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。
那什么是图层呢?相信用过 Photoshop 的小伙伴对图层并不陌生。我们也可以在Chrome浏览器的开发者工具中,选择Layers标签,就可以看到页面的分层情况,以掘金为例,其分层情况如下:
可以看到,渲染引擎给页面分了很多图层,这些图层会按照一定顺序叠加在一起,就形成了最终的页面。 通常情况下,并不是渲染树上的每个节点都包含一个图层,如果一个节点没有对应的图层,那这个节点就会属于其父节点的图层。那什么样的节点才能让浏览器引擎为其创建一个新的图层呢?需要满足以下其中一个条件:
(1)拥有层叠上下文属性的元素
对于上图,由上到下分别是:
- 背景和边框:建立当前层叠上下文元素的背景和边框。
- 负的z-index:当前层叠上下文中,z-index属性值为负的元素。
- 块级盒:文档流内非行内级非定位后代元素。
- 浮动盒:非定位浮动元素。
- 行内盒:文档流内行内级非定位后代元素。
- z-index:0:层叠级数为0的定位元素。
- 正z-index:z-index属性值为正的定位元素。
(2)需要裁剪的元素
什么是裁剪呢?假如有一个固定宽高的div盒子,而里面的文字较多超过了盒子的高度,这时就会产生裁剪,浏览器渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。当出现裁剪时,浏览器的渲染引擎就会为文字部分单独创建一个图层,如果出现滚动条,那么滚动条也会被提升为单独的图层。
2. 绘制图层
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,下面就来看看渲染引擎是怎么实现图层绘制的。
- 渲染引擎在绘制图层时,会把一个图层的绘制分成很多绘制指令,然后把这些指令按照顺序组成一个待绘制的列表
主线程的光栅和合成
当确定了绘制顺序,主线程就会将该信息提交给合成器线程。然后合成器线程对每一层进行光栅化。图层可能像页面的整个长度一样大,甚至超过视口区,因此合成器线程会将它们分成图块并将每个图块发送到光栅线程。光栅线程对每个图块进行光栅化并将其存储在 GPU 内存中。
简单来说,将页面分解成多个图层的操作就成为分层, 最后将这些图层合并到一层的操作就成为合成
合成
是一种将页面的各个部分分成图层
、分别光栅化
并在渲染引擎中的合成线程
的单独线程中合成为页面的技术。如果发生滚动,由于图层已经光栅化,它所要做的就是合成一个新帧。并且可以通过移动图层并合成新帧以相同的方式实现动画。
那什么叫光栅化呢?简单来说,就是将这些信息转换成屏幕上的像素。我们可以用下面的动画所示:
一旦图块被光栅化,合成线程就会收集称为"绘制四边形" 的图块信息来创建合成器框架。
然后,合成器框架通过 IPC
提交给浏览器进程。此时,可以从 UI 线程添加另一个合成器框架
以进行浏览器 UI 更改
,或者从其他渲染器进程添加以进行扩展。这些合成器帧
被发送到 GPU
以将其显示在屏幕上。如果出现滚动事件,合成器线程将创建另一个合成器帧
发送到 GPU。
最终,内存显示在屏幕上,这样就完成了页面的绘制。
至此,整个渲染流程就完成了,其过程总结如下:
- 将HTML内容构建成DOM树;
- 将CSS内容构建成CSSOM树;
- 将DOM 树和 CSSOM 树合成渲染树;
- 根据渲染树进行页面元素的布局;
- 对渲染树进行分层操作,并生成分层树;
- 为每个图层生成绘制列表,并提交到合成线程;
- 合成线程将图层分成不同的图块,并通过栅格化将图块转化为位图;
- 合成线程给浏览器进程发送绘制图块指令;
- 浏览器进程会生成页面,并显示在屏幕上。