DOM树是如何生成的

简介

当我们浏览互联网上的网页时,页面上的内容并不是凭空出现的,而是经过一系列的过程生成的。这个过程的核心就是浏览器内部的 DOM 树的构建。

文档对象模型(The Document Object Model)即我们常说的DOM,是一种表示文档结构的树状数据结构,它将整个 HTML 文档以及其中的元素、属性、文本内容等抽象成一个有层次结构的树形模型。DOM 对于创建可以与用户输入交互并实时响应更改的动态网页和 Web 应用程序至关重要。

在了解DOM树的创建之前,我们先了解一下浏览器渲染引擎的工作原理。

浏览器渲染引擎的工作原理

通常用户用户通过在地址栏输入一个 URL、点击一个链接、提交表单或者是其他的行为,通过浏览器向名称服务器发起 DNS 查询请求,最终得到一个 IP 地址,然后通过TCP三次握手,TLS协商来建立安全的连接,浏览器会发送一个HTTP的get请求,对于网站来说,这个请求通常是一个 HTML 文件,接下来,浏览器就需要解析这个文件进行页面的渲染。

  1. HTML 解析(构建DOM树): 浏览器渲染引擎首先会对 HTML 文档进行解析,将 HTML 标记解析成 DOM 树。这个过程包括词法分析和语法分析,生成 DOM 树的结构。
  2. CSS 解析(构建 CSSOM 树): 在 HTML 解析完成后,渲染引擎会进行 CSS 解析,将样式表解析成样式规则。这个过程包括解析样式表中的选择器和属性,然后将它们应用到 DOM 树的节点上,形成带有样式的 DOM 树。
  3. 构建渲染树: 渲染引擎将 DOM 树和样式规则结合,构建出渲染树(Render Tree)。渲染树是一个只包含可视化内容的树,它忽略了不可见的元素(例如 <head>display: none 的元素等)。渲染树中的每个节点都包含了节点的样式信息。
  4. 布局(回流): 渲染引擎根据渲染树的结构和样式信息,计算每个节点在屏幕上的位置和大小,进行布局(也叫回流)。这个过程中会考虑盒模型、浮动、定位等因素,确定每个元素的准确位置。
  5. 绘制(重绘) 根据计算得到的布局信息,渲染引擎开始将每个节点绘制成屏幕上的像素。这个过程包括绘制背景、颜色、边框等,形成绘制表面。
  6. 合成: 绘制完成后,渲染引擎将各个层的绘制表面合成为页面上的一张图像。这个过程可能包括图层的合并、透明度的处理等。
  7. 显示: 最后,渲染引擎将合成后的图像显示在用户的屏幕上。这个过程也包括浏览器的其他界面元素的显示,例如地址栏、书签栏等。

这个过程是逐步发生的,而不是一次性完成的。浏览器通过不断的重复这个过程来实现页面的实时渲染和交互。这也是为什么对于性能优化来说,减少回流和重绘的次数是非常重要的,因为它们是相对较昂贵的操作。

DOM 树如何生成

  1. HTML 解析: 浏览器首先会对 HTML 文档进行解析,解析器会读取 HTML 文档中的字符流,将其转换为一个个标记(token)。
  2. 构建 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
└──
  1. 构建父子关系: 解析器通过遇到开始标签和结束标签来确定元素之间的父子关系。例如,遇到一个开始标签就创建一个元素节点,并将其作为当前节点的子节点,遇到结束标签就将当前节点移回到其父节点。
  2. 属性设置: 每个元素节点可以包含属性,解析器会读取标签中的属性,并将其设置为相应节点的属性。
  3. 文本节点: 文本内容也被解析成文本节点,它们作为元素节点的子节点存在。
  4. 解析完成: 当整个 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> 标签设置为异步加载(asyncdefer 属性)。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!');
});
  1. 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 树的结构。
相关推荐
摸鱼仙人~33 分钟前
styled-components:现代React样式解决方案
前端·react.js·前端框架
sasaraku.1 小时前
serviceWorker缓存资源
前端
RadiumAg2 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo2 小时前
ES6笔记2
开发语言·前端·javascript
yanlele2 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试
中微子4 小时前
React状态管理最佳实践
前端
烛阴4 小时前
void 0 的奥秘:解锁 JavaScript 中 undefined 的正确打开方式
前端·javascript
中微子4 小时前
JavaScript 事件与 React 合成事件完全指南:从入门到精通
前端
Hexene...4 小时前
【前端Vue】如何实现echarts图表根据父元素宽度自适应大小
前端·vue.js·echarts
初遇你时动了情4 小时前
腾讯地图 vue3 使用 封装 地图组件
javascript·vue.js·腾讯地图