HTML解析阶段是如何生成DOM树和CSSOM树?
一、构建DOM树
在说构建 DOM 树之前,我们首先需要知道,为什么要构建 DOM 树呢? 这是因为,浏览器是无法直接理解和使用 HTML 的,所以需要将 HTML 转化为浏览器能够理解的结构——DOM树。
在页面中,每个 HTML 标签都会被浏览器解析成文档对象,HTML 本质上就是一个嵌套结构,在解析时会把每个文档对象用一个树形结构组织起来,所有的文档对象都会挂在 document 上,这种组织方式就是 HTML 最基础的结构——文档对象模型(DOM),这棵树的每个文档对象就叫做DOM节点。
在渲染引擎中,DOM 有三个层面的作用:
- 从页面的视角来看,DOM 是生成页面的基础数据结构
- 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容
- 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段会被拒之门外
在渲染引擎内部,HTML 解析器负责将 HTML 字节流转换为 DOM 结构,其转化过程如下:
1. 字符流 → 词(token)
HTML结构会首先通过分词器将字符流拆分为词(token),Token分为 Tag Token 和文本 Token,下面来看一个HTML代码是如何被拆分的:
<body>
<div>
<p>hello world</p>
</div>
</body>
对于这句代码,可以拆成词: 可以看到,Tag Token 又分 StartTag 和 EndTag,
<body>
、<div>
、<p>
就是 StartTag ,</body>
、</div>
、</p>
就是 EndTag,分别对应图中的蓝色和红色块,文本 Token 对应绿色块。
这里会通过状态机将字符拆分成 token,所谓的状态机就是将每个词的特征逐个拆分成独立的状态,然后再将所有词的特征字符合并起来,形成一个连通的图结构。那为什么要使用状态机呢?因为每读取一个字符,都要做一次决策,这些决策都和当前的状态有关。
实际上,状态机的作用就是用来做词法分析的,将字符流分解为词(token)。
2. 词(token)→ DOM树
接下来就需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。这个过程是通过栈结构来实现的,这个栈主要用来计算节点之间的父子关系,上面步骤中生成的 token 会按顺序压入栈中,该过程的规则如下:
- 如果分词器解析出来是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点;
- 如果分词器解析出来是 文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点;
- 如果分词器解析出来的是 EndTag Token,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div从栈中弹出,表示该 div 元素解析完成。
通过分词器产生的新 Token 就这样不停地入栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。
下面来看看这的Token栈是如何工作的,有如下HTML结构:
开始时,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,如下图所示:
再之后的过程就和上面类似了,最终的结果如下:
二、构建CSSOM树
上面已经基本了解了 DOM 的构建过程,但是这个 DOM 结构只包含节点,并不包含任何的样式信息。下面就来看看,浏览器是如何把 CSS 样式应用到 DOM 节点上的。
同样,浏览器也是无法直接理解 CSS 代码的,需要将其浏览器可以理解的 CSSOM 树。实际上,浏览器在构建 DOM 树的同时,如果样式也加载完成了,那么 CSSOM 树也会同步构建,CSSOM 树和 DOM 树类似,它主要有两个作用:
- 提供给 JavaScript 操作样式的能力
- 为渲染树的合成提供基础的样式信息
不过,CSSOM 树和 DOM 树是独立的两个数据结构,它们并没有一一对应关系,DOM 树描述的是 HTML 标签的层级关系,CSSOM 树描述的是选择器之间的层级关系。可以在浏览器的控制台,通过 document.styleSheets
命令来查看 CSSOM 树: 那 CSS 样式的来源有哪些呢?
可以看到,CSS 样式的来源主要有三种:
- 通过 link 引用的外部 CSS 样式文件
<style>
标签内的CSS样式- 元素的 style 属性内嵌的 CSS
在将 CSS 转化为树形对象之前,还需要将样式表中的属性值进行标准化处理,比如,当遇到以下 CSS 样式:
body { font-size: 2em }
p {color:blue;}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
可以看到上面 CSS 中有很多属性值,比如 2em、blue、red、bold等,这些数值并不能被浏览器直接理解。所以,需要将所有值转化为浏览器渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。经过标准化的过程,上面的代码会变成这样:
body { font-size: 32px }
p {color: rgb(0, 0, 255);}
div {font-weight: 700}
div p {color: (0, 128, 0);}
div {color: (255, 0, 0); }
可以看到,2em 被解析成了32px,blue 被解析成了rgb(255, 0, 0),bold 被解析成 700。现在样式的属性已被标准化了,接下来就需要计算 DOM 树中每个节点的样式属性了,这就涉及到 CSS 的继承规则和层叠规则。
1. 样式继承
在 CSS 中存在样式的继承机制,CSS 继承就是每个 DOM 节点都包含有父节点的样式。比如在 HTML 上设置“font-size:20px;” 那么页面里基本所有的标签都可以继承到这个属性了。
在 CSS 中,有继承性的属性主要有以下几种:
- 字体系列属性
- font-family:字体系列
- font-weight:字体的粗细
- font-size:字体的大小
- font-style:字体的风格
- 文本系列属性
- text-indent:文本缩进
- text-align:文本水平对齐
- line-height:行高
- word-spacing:单词之间的间距
- letter-spacing:中文或者字母之间的间距
- color:文本颜色
- 元素可见性
- visibility:控制元素显示隐藏
- 列表布局属性
- list-style:列表风格,包括list-style-type、list-style-image等
- 光标属性
- cursor:光标显示为何种形态
2. 样式层叠
样式计算过程中的第二个规则是样式层叠,层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。
总之,样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程 中需要遵守 CSS 的继承和层叠两个规则,这个阶段最终输出的内容是每个 DOM 节点的样 式,并被保存在 ComputedStyle 的结构内。
对于以下代码:
最终生成的 CSSOM 树大致如下:
转载自:https://juejin.cn/post/7295681756743647243