开篇
在上节课中我们介绍了用户发出 URL 请求到页面开始解析的这个过程,一旦文档被提交,便进入了渲染阶段。渲染进程开始页面解析和子资源加载了。这个阶段很重要,了解其相关流程能让你“看透”页面是如何工作的,有了这些知识,你可以解决一系列相关的问题,比如能熟练使用开发者工具,能优化页面卡顿问题,使用 JavaScript 优化动画流程 等等。
我们在HTTP请求流程中跟大家讲过渲染流水线。
流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。接下来我们就按照这个流水线来跟大家解释每个子阶段都在做哪些事。
构建 DOM 树
为什么要构建 DOM 树呢?
这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
那什么是DOM?
从网络进程传给渲染进程的 HTML 格式字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。DOM 提供了对 HTML 文档结构化的表述。在渲染引擎中,DOM 有三个层面的作用。
一: 从页面的视角来看,DOM 是生成页面的基础数据结构。
二:从 JavaScript 视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
三:从安全视角来看,DOM 是一道安全防护线,把一些不安全的内容在 DOM 解析阶段就被拒之门外了。
总结成一句话,DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。
什么是树结构?
为了让大家能更直观地理解,你可以参考下面我画的这张树结构图:
从图中可以看出,树这种结构非常像我们现实生活中的“树”,其中每个点我们称为节点,最上层的是根节点,下层的分支称为子节点,相连的节点称为父子节点, 同一层级的节点称为 兄弟节点。
DOM 树如何生成
在渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。
HTML 解析器是如何工作的呢?
网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如content-type的值是text/html,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器。
你可以把这个管道想象成一个“水管”,网络进程接收到的字节流像水一样倒进这个“水管”,而“水管”的另外一端是渲染进程的 HTML 解析器,它会动态接收字节流,并将其解析为 DOM。
特别注意: HTML 解析器并不会等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。
DOM 的具体生成流程
接下来我们就来详细聊聊 DOM 的具体生成流程。代码从网络传输过来是字节流的形式,那么后续字节流是如何转换为 DOM 的呢?
从图中你可以看出,字节流转换为 DOM 需要三个阶段。
第一个阶段,分词器先将字节流转换为一个个Token,分为 Tag Token 和文本 Token。上述 HTML 代码通过词法分析生成的Token如下所示:
词法分析是计算机科学中将字符序列转换为标记(token)序列的过程。 词法规定了语言的最小语义单元:token。
由图可以看出,Tag Token又分StartTag 和 EndTag,比如就是StartTag,就是 EndTag,分别对于图中的蓝色和红色块,文本Token 对应的绿色块。
后续的第二个和第三个阶段是同步进行的,需要将Token解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
HTML 解析器维护了一个Token栈结构,该Token栈主要用来计算节点之间的父子关系,在第分词器中生成的Token会被按照顺序压到这个栈中。具体的处理规则大家看图:
这里需要补充说明下,HTML 解析器开始工作时,会默认创建了一个根为document的空 DOM 结构,同时会将一个StartTag document的Token压入栈底。然后经过分词器解析出来的第一个StartTag html Token会被压入到栈中,并创建一个html的 DOM 节点,添加到document 上。
为了更加直观地理解整个过程,下面我们结合一段 HTML 代码,来一步步分析 DOM 树的生成过程。
<html>
<body>
<div>1</div>
<div>test</div>
</body>
</html>
这段代码以字节流的形式传给了 HTML 解析器,经过分词器处理解析出来的第一个Token 顺序如下图。
然后按照同样的流程解析出来StartTag body和StartTag div,其Token栈和 DOM 的状态如下图所示:
大家发现什么规律了吗?
如果压入到栈中的是StartTag Token,HTML 解析器会为该Token创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。 (div 父节点是body , body的父节点是HTML)。
继续解析:接下来解析出来的是第一个div的文本Token,渲染引擎会为该Token创建一个文本节点,并将该Token添加到 DOM 中看图:
大家发现了什么吗?
如果分词器解析出来是文本Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本Token是不需要压入到栈中,它的父节点就是当前栈顶Token所对应的 DOM 节点。
继续:再接下来,分词器解析出来第一个EndTag div,这时候 HTML 解析器会去判断当前栈顶的元素是否是StartTag div,如果是则从栈顶弹出StartTag div,表示该 div 元素解析完成。如下图所示:
按照同样的规则,一路解析,最终结果如下图所示:
通过分词器产生的新Token就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。
有了上面的分析,相信你已经清楚 DOM 是怎么生成的了。不过在实际生产环境中,HTML 源文件中既包含 CSS 和 JavaScript,又包含图片、音频、视频等文件,处理过程远比上面这个示范 Demo 复杂。不过理解了这个简单的 Demo 生成过程,我们就可以往下分析更加复杂的场景了。
样式计算
现在我们已经生成 DOM 树了,但是 DOM 节点的样式我们依然不知道,要让 DOM 节点拥有正确的样式,这就需要样式计算了。
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
1. 把 CSS 转换为浏览器能够理解的结构
那 CSS 样式的来源主要有哪些呢?
从图中可以看出,CSS 样式来源主要有三种:
- 通过 link 引用的外部 CSS 文件