在上一篇文章中,介绍了 浏览器的事件循环,其中提到了浏览器的进程模型。那浏览器是如何渲染页面的呢?
浏览器会通过网络进程中的线程来通信,获取到 html 数据后生成渲染任务,发送给消息队列。
渲染主线程会执行渲染任务。整个渲染流程:把 html 字符串解析为像素点信息,再交给 GPU来渲染后在页面中展示。
每个阶段都有明确的输入输出,上个阶段的输出会成为下个阶段的输入。形成一套完整的流水线。
会将 html 字符串解析为 2个树。因为字符串不好操作,对象更容易处理。
也就是 document
对象。可以在控制台通过console.dir(document)
展示对象结构。
包括:
内部样式
外部样式style=""
内联样式注意,这里的 CSSOM树 ≠ document.styleSheets
。因为 document.styleSheets
只包括内部样式和外部样式,每写一个 或
就会多一个
CSSStyleSheet
样式表:
举例说明:
<html>
<head>
<style>
body h1 {
color: red;
font-size: 3em;
}
div p {
margin: 1em;
color: blue;
}
style>
head>
<body>
<h1>下雪天的夏风h1>
<div>
<p>求关注p>
div>
body>
html>
可以看到:
CSSStyleSheet
样式表
CSSStyleRule
规则对象
selectorText
选择器style
样式(键值对)另外,CSSStyleSheet
样式表是可以直接通过 js 操作的。
举例:通过 js 给页面所有 div 添加 border
document.styleSheets[0].addRule('div', 'border: 1px solid !important')
这样添加样式的方式,一般框架用的多。最终样式不会在内联样式中展现。
渲染主线程遇到 css 时,会启动一个预解析线程,让它来率先下载和解析 css。渲染主线程继续解析 html。
预解析线程会快速浏览,如果遇到外部样式link
,会通知网络线程来下载 css,下载完成后进行“解析”完成后交给渲染主线程。
并不是真正的解析,只是做一些前期工作,最终生成 CSSOM 树还是由渲染主线程来完成。
所以,css 代码不会阻塞解析 HTML。
没有生成所谓的 js 树,是因为 js 只需要执行一遍即可。DOM树和CSSOM树作为解析 HTML 的输出,后续还会有其他的操作。
渲染主线程遇到内部 js 时,直接启动 V8 引擎执行即可;遇到外部 js 时,会启动一个预解析线程,让它来下载 js,渲染主线程暂停。
预解析线程会通知网络线程来下载 js,下载完成后再交给渲染主线程来执行。执行完继续解析 HTML。
这样做的原因:DOM 树是边解析边生成的,而 js 代码可能会修改之前已解析好的内容。
所以,js 代码会阻塞解析 HTML。
遍历DOM树,计算每个节点的最终样式 Computed Style。
这一过程,许多预设值会变成绝对值,比如 red
变为 rgb(255,0,0)
;相对单位变为绝对单位,比如rem
变为 px
可以在浏览器的 computed 窗口中,或使用 getComputedStyle() 查看某个元素的最终样式。
遍历DOM树的每个节点,根据 css 属性值计算每个节点的几何信息(尺寸,相对包含块的位置),生成一个 layout 树。
注意到 DOM 树和 layout 树不一定一一对应。
举例1:diaplay:none
的元素不会出现在 layout 树中。
问题来了,为什么
等元素默认是隐藏的?因为在浏览器默认样式表中,它们
diaplay:none
!
举例2:伪元素的 content
内容在 DOM 树中没有,在 layout 树中有。
举例3:内容必须在行盒中,行盒和块盒不能相邻。所以在 layout 树中会有匿名块盒。
解释:“行级元素”,“块级元素”这些元素指的是 html。但元素的类型是 css 属性决定的。所以称为行盒或块盒。
现在 layout 布局树中每个节点的几何信息,尺寸位置等都明确了。渲染主线程会使用一套策略对整个布局树分层。
目的是提升效率,这样可以让之后页面的修改更新不会影响到其他层。类似 PS 中的图层,修改某一个图层不会影响到其他图层。
可以在浏览器控制台的
Layers
面板查看当前网页的分层信息。
也不会分太多的层,因为会比较占内存。滚动条是单独一层。
另外,和堆叠上下文有关的 css 属性(transform,opacity)会影响分层的决策。其中 will-change
属性能更大程度的影响分层角色,可能会将设置该属性的元素单独分一层。
因为这个属性会告诉浏览器,我可能会经常变化,浏览器最好掂量下。
分层后,会对每层都生成绘制指令,类似于 canvas 中的 API 一样。其实canvas 用的就是浏览器内核的绘制功能。
指令举例:“笔”移动到 xx 坐标位置,画 100*100 的矩形,并用红色填充等等。
以上。渲染主线程的工作结束,剩下的步骤交给其他线程来完成。
将每层都分为多个小的区域,浏览器视窗区域的优先级最高,靠近视窗区域的优先级次之。
分块逻辑:渲染主线程每个图层的绘制信息交给合成线程,合成线程又会启动多个分块线程来对每个图层进行分块。
合成线程也属于渲染线程
将每个块变成位图,位图就是每个像素点的信息。优先处理靠近视窗的块。
位图就是内存中的二位数组,其中记录了每个像素点的信息。
合成线程现在拿到了所有的信息,在画之前还需要确认【指引信息 quad】,也就是位图相对的屏幕在哪里。
注意,之前布局树中的信息是相对于整个页面的。现在需要知道每个块相对于屏幕的位置信息。
步骤:合成线程将指引信息 --> GPU 进程 --> 硬件显卡,由显卡来呈现最终的像素信息。
GPU 做中转的原因是:GPU 是浏览器的进程。合成线程属于渲染进程,它是在沙盒中的,与硬件系统做隔离,提升安全性。所以渲染进程是没有调度系统硬件能力的。
而 css 中的 transform 属性就是在这一步完成的。这就是 transform 效率高的原因,直接跳过之前所有的步骤。
【recalculate layoutBlockFlow 重排】它的本质是重新计算 layout 树。
当做了会影响 layout 树的操作后,比如修改几何尺寸相关的信息时,会引起重新计算 layout 树。
并且,为了避免连续多次的操作导致 layout 树反复计算,浏览器会合并这些操作,当 js 代码完成后统一计算。所以这一步骤是异步的。
同样因为这个原因,当 js 获取布局属性时,可能无法获取到最新的布局信息。
浏览器会在反复权衡下,最终决定获取属性时立即 reflow。
它的本质是重新根据分层信息计算了绘制指令。
当改变了可见样式,比如颜色等和几何尺寸无关的属性时,就需要重新计算,会从【绘制】这一步骤开始重新执行。
而因为几何尺寸也属于可见样式,所以 reflow 一定会引起 repaint。
因为 transform 既不会影响布局,也不会影响绘制,它只会影响渲染的最后一步【画】。而【画】是在合成线程中,不会影响到渲染主线程。同样无论渲染主线程多忙,也不会影响到 transform。
验证代码如下,当渲染主线程卡死时,transform 不受影响。
DOCTYPE html>
<html lang="en">
<head>
<style>
.common {
width: 50px;
height: 50px;
background-color: salmon;
border-radius: 50%;
margin: 10px;
}
.ball1 {
animation: move1 1s alternate infinite;
}
.ball2 {
position: relative;
left: 0;
animation: move2 1s alternate infinite;
}
@keyframes move1 {
to {
transform: translate(100px);
}
}
@keyframes move2 {
to {
left: 100px;
}
}
style>
head>
<body>
<button id="btn">死循环3sbutton>
<div class="common ball1">div>
<div class="common ball2">div>
<script>
btn.addEventListener("click", function () {
delay(3000);
});
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
script>
body>
html>
滚动也是一样的逻辑,如果 js 有一段上面的死循环,并不会影响到滚动。因为只有视窗内元素的位置变了,直接执行【画 draw】这一步骤。
以上。
参考:渡一教育。