简介
当我们浏览互联网上的网页时,页面上的内容并不是凭空出现的,而是经过一系列的过程生成的。这个过程的核心就是浏览器内部的 DOM 树的构建。
文档对象模型(The Document Object Model)即我们常说的DOM,是一种表示文档结构的树状数据结构,它将整个 HTML 文档以及其中的元素、属性、文本内容等抽象成一个有层次结构的树形模型。DOM 对于创建可以与用户输入交互并实时响应更改的动态网页和 Web 应用程序至关重要。
在了解DOM树的创建之前,我们先了解一下浏览器渲染引擎的工作原理。
浏览器渲染引擎的工作原理
通常用户用户通过在地址栏输入一个 URL、点击一个链接、提交表单或者是其他的行为,通过浏览器向名称服务器发起 DNS 查询请求,最终得到一个 IP 地址,然后通过TCP三次握手,TLS协商来建立安全的连接,浏览器会发送一个HTTP的get请求,对于网站来说,这个请求通常是一个 HTML 文件,接下来,浏览器就需要解析这个文件进行页面的渲染。
- HTML 解析(构建DOM树): 浏览器渲染引擎首先会对 HTML 文档进行解析,将 HTML 标记解析成 DOM 树。这个过程包括词法分析和语法分析,生成 DOM 树的结构。
- CSS 解析(构建 CSSOM 树): 在 HTML 解析完成后,渲染引擎会进行 CSS 解析,将样式表解析成样式规则。这个过程包括解析样式表中的选择器和属性,然后将它们应用到 DOM 树的节点上,形成带有样式的 DOM 树。
- 构建渲染树: 渲染引擎将 DOM 树和样式规则结合,构建出渲染树(Render Tree)。渲染树是一个只包含可视化内容的树,它忽略了不可见的元素(例如
<head>
、display: none
的元素等)。渲染树中的每个节点都包含了节点的样式信息。 - 布局(回流): 渲染引擎根据渲染树的结构和样式信息,计算每个节点在屏幕上的位置和大小,进行布局(也叫回流)。这个过程中会考虑盒模型、浮动、定位等因素,确定每个元素的准确位置。
- 绘制(重绘) 根据计算得到的布局信息,渲染引擎开始将每个节点绘制成屏幕上的像素。这个过程包括绘制背景、颜色、边框等,形成绘制表面。
- 合成: 绘制完成后,渲染引擎将各个层的绘制表面合成为页面上的一张图像。这个过程可能包括图层的合并、透明度的处理等。
- 显示: 最后,渲染引擎将合成后的图像显示在用户的屏幕上。这个过程也包括浏览器的其他界面元素的显示,例如地址栏、书签栏等。
这个过程是逐步发生的,而不是一次性完成的。浏览器通过不断的重复这个过程来实现页面的实时渲染和交互。这也是为什么对于性能优化来说,减少回流和重绘的次数是非常重要的,因为它们是相对较昂贵的操作。
DOM 树如何生成
- HTML 解析: 浏览器首先会对 HTML 文档进行解析,解析器会读取 HTML 文档中的字符流,将其转换为一个个标记(token)。
- 构建 DOM 节点: 解析器根据标记构建 DOM 节点。每个 HTML 元素(标签)、文本内容、注释等都被表示为一个 DOM 节点。这些节点之间通过父子关系连接起来,构成了树状结构。
例如以下的HTML代码:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM Tree</title>
</head>
<body>
<header>
<h1>Welcome to DOM Tree</h1>
</header>
<section>
<p>DOM tree</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</section>
<footer>
<p>DOM End</p>
</footer>
</body>
</html>
解析之后的DOM树结构如下:
js
Document
├── html
│ ├── head
│ │ ├── meta
│ │ ├── meta
│ │ └── title
│ ├── body
│ ├── header
│ │ └── h1
│ ├── section
│ │ ├── p
│ │ └── ul
│ │ ├── li
│ │ ├── li
│ │ └── li
│ └── footer
│ └── p
└──
- 构建父子关系: 解析器通过遇到开始标签和结束标签来确定元素之间的父子关系。例如,遇到一个开始标签就创建一个元素节点,并将其作为当前节点的子节点,遇到结束标签就将当前节点移回到其父节点。
- 属性设置: 每个元素节点可以包含属性,解析器会读取标签中的属性,并将其设置为相应节点的属性。
- 文本节点: 文本内容也被解析成文本节点,它们作为元素节点的子节点存在。
- 解析完成: 当整个 HTML 文档解析完成后,浏览器就构建了完整的 DOM 树。
JavaScript和css对DOM树的影响
1. JavaScript对DOM树的影响
- 解析过程中的阻塞
当 HTML 解析器遇到 <script>
标签时,它会采取暂停解析的策略,这是因为 JavaScript 可能会修改当前已经生成的 DOM 结构,而 HTML 解析器无法理解和执行 JavaScript 代码。这个暂停解析的过程通常被称为 "HTML 解析阻塞"。
在 HTML 解析器暂停的时候,浏览器会创建一个任务,并将 JavaScript 代码交给 JavaScript 引擎执行。JavaScript 引擎执行完代码后,会通知浏览器继续 HTML 解析。这个过程确保 JavaScript 的执行不会阻塞 HTML 解析,而 HTML 解析也不会阻塞 JavaScript 的执行。
例如:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM Tree</title>
</head>
<body>
<div id="content">
Welcome to DOM Tree
</div>
<script>
// JavaScript 修改 DOM,导致 HTML 解析阻塞
document.getElementById('content').innerText = 'Updated Content';
</script>
</body>
</html>
但是对于从外部引入的js文件,流程基本一致,也是遇到 <script>
标签时采取暂停解析的策略,不同的是还需要将该文件下载下来后再执行 JavaScript 代码。不过Chrome对外部引入的文件,采取了预解析操作,当分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
- 异步加载
为了避免阻塞,可以将 <script>
标签设置为异步加载(async
或 defer
属性)。async
表示脚本会异步执行,对JavaScript加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前,而 defer
则表示脚本会在文档解析完成后执行,但在 DOMContentLoaded
事件之前执行。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Async Script Example</title>
<!-- 使用 async 属性使得脚本异步加载,不会阻塞页面渲染。 -->
<script async>
// 异步加载的脚本
console.log('Async Script Executed');
</script>
</head>
<body>
<div>Page Content</div>
</body>
</html>
- 操作DOM的api
动态创建节点、节点的属性和样式修改、节点的删除和替换、事件处理都会对DOM产生影响
动态创建节点:
js
// 创建新的 <div> 元素
const newDiv = document.createElement('div');
// 设置元素的内容
newDiv.innerText = 'This is a dynamically created div.';
// 将新元素添加到文档中
document.body.appendChild(newDiv);
节点的属性和样式修改:
js
// 获取已有元素
const existingDiv = document.getElementById('existing-div');
// 修改属性
existingDiv.setAttribute('title', 'Updated Title');
// 修改样式
existingDiv.style.color = 'red';
节点的删除和替换:JavaScript 允许删除现有节点或者替换节点。
js
// 获取已有元素
const outdatedDiv = document.getElementById('outdated-div');
// 删除节点
outdatedDiv.parentNode.removeChild(outdatedDiv);
// 替换节点
const newParagraph = document.createElement('p');
newParagraph.innerText = 'This is a new paragraph.';
outdatedDiv.parentNode.replaceChild(newParagraph, outdatedDiv);
事件处理:JavaScript 可以为元素添加事件监听器,实现对用户交互的响应。
js
// 获取按钮元素
const myButton = document.getElementById('my-button');
// 添加点击事件监听器
myButton.addEventListener('click', function() {
alert('Button Clicked!');
});
- css对DOM树的影响
本质上,CSS 确实不会阻塞 DOM 的解析,但它会影响到渲染过程中的其他步骤,包括样式计算、布局和绘制。这些过程可能会影响 JavaScript 的执行,并且在某些情况下可能阻止 DOM 树的解析。
- Render 树的构建
浏览器在解析 HTML 和 CSS 后,会构建 Render 树,它是 DOM 树和 CSSOM 树的结合,用于描述页面的渲染结构。JavaScript 和 CSS 的修改都会触发 Render 树的重新构建。
下面这段js代码执行之前,需要确保相关的样式已经被解析生成 CSSOM,如果代码引用了外部的 CSS 文件,确实需要等待外部 CSS 文件下载完成并解析生成 CSSOM 对象。如果 JavaScript 代码依赖于样式(例如,操纵了样式),那么浏览器会尽量提前获取和处理这些样式,以避免 JavaScript 在执行过程中出现样式相关的问题。
js
// 获取已有元素
const styledElement = document.getElementById('styled-element');
// 修改样式
styledElement.style.backgroundColor = 'yellow';
styledElement.style.fontSize = '20px';
- 样式计算
当 JavaScript 修改元素的样式时,浏览器需要重新计算样式。这包括计算继承、层叠和覆盖规则等,确保页面的最终样式能够正确应用。
- 重绘和重排: 样式的修改可能触发页面的重绘和重排。重绘是指更新元素的视觉效果而不改变其布局,而重排是指改变了布局,需要重新计算元素的几何属性。这两个过程都会影响到 Render 树的结构。