什么是DOM?
DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。
为什么需要DOM?
浏览器引擎无法识别HTML文件字节流,所以需要将HTML文件字节流转换成浏览器引擎识别的内部结构,它就是DOM。
DOM如何使用?
DOM提供了HTML文档结构化的表述。
DOM提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
DOM树如何生成?
HTML 字节流转换为 DOM 结构,需要经过HTML解析器的处理。HTML解析器是浏览器渲染引擎内部的一个模块。
HTML解析器的工作就是将网络或者本地磁盘获取的HTML网页和资源从字节流解析成DOM树结构:
- 网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件。
- 然后为该请求选择或者创建一个渲染进程。
- 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道。
- 网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据丢给 HTML 解析器。
- 字节流转换为 DOM 需要三个阶段。
- 阶段一,通过分词器先将字节流转换为一个个 Token,分为 Tag Token 和文本 Token。Tag Token 又分 StartTag 和 EndTag。
- 第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。HTML 解析器维护了一个 Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。
解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag document 的 Token 压入栈底。
然后经过分词器解析出来的第一个 StartTag html Token 会被压入到栈中,并创建一个 html 的 DOM 节点,添加到 document 上。
通过分词器产生的新 Token 就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。
举个?:
<html>
<body>
<div>吃饭</div>
<div>睡觉</div>
</body>
</html>
复制代码
- 先创建根为 document 的空 DOM 结构,然后将 StartTag document 的 Token 压入栈底。
- 分词器解析出StartTag html,然后创建 html的DOM节点,挂载到document下, 然后将 StartTag html 的 Token 入栈。
- 分词器解析出StartTag body,然后创建 body的DOM节点,挂载到html下,然后将 StartTag body 的 Token 入栈。
- 分词器解析出StartTag div,然后创建 div的DOM节点,挂载到body下,然后将 StartTag div 的 Token 入栈。
- 分词器解析出div 的文本 Token,然后创建文本节点,挂载到div下,文本 Token 不需要压入到栈中。
- 分词器解析出EndTag div,这时候 HTML 解析器会去判断当前栈顶的元素是否是 StartTag div,如果是则从栈顶弹出 StartTag div。
- 分词器解析出StartTag div,然后创建 div的DOM节点,挂载到body下,然后将 StartTag div 的 Token 入栈。
- 分词器解析出div 的文本 Token,然后创建文本节点,挂载到div下,文本 Token 不需要压入到栈中。
- 分词器解析出EndTag div,这时候 HTML 解析器会去判断当前栈顶的元素是否是 StartTag div,如果是则从栈顶弹出 StartTag div。
- 分词器解析出EndTag body,这时候 HTML 解析器会去判断当前栈顶的元素是否是 StartTag body,如果是则从栈顶弹出 StartTag body。
- 分词器解析出EndTag html,这时候 HTML 解析器会去判断当前栈顶的元素是否是 StartTag html,如果是则从栈顶弹出 StartTag html。
- 最后document 出栈。
如果两段 div 中间插入了一段 JavaScript 脚本,那就有点儿小变化。
<html>
<body>
<div>吃饭</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = '打游戏'
</script>
<script type="text/javascript" src='foo.js></script>
<div>睡觉</div>
</body>
</html>
复制代码
- 分词器解析到第一条script 时。HTML解析器会暂停下工作,JavaScript 引擎介入,执行 script 标签中的这段脚本。
- 分词器解析到第二条script 时。需要先下载这段 JavaScript 代码。这会需要重点关注下载环境,因为 JavaScript 文件的下载过程会阻塞 DOM 解析,通常下载很耗时,而且会受到网络环境、JavaScript 文件大小等因素的影响。
- 在chrome 浏览器中做了优化,会有一个预解析操作,渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
- 还可以使用 CDN 来加速 JavaScript 文件的加载。
- 压缩 JavaScript 文件的体积。
- 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。
- 脚本执行完成后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。
如果有引用外部 CSS 文件,或者通过 style 标签内置了 CSS 内容,那么渲染引擎还需要将这些内容转换为 CSSOM。因为 JavaScript 有修改 CSSOM 的能力,所以在执行 JavaScript 之前,还需要依赖 CSSOM。所以 CSS 在部分情况下也会阻塞 DOM 的生成。
HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM。
<html>
<head>
<link href="theme.css" rel="stylesheet">
</head>
<body>
<div>吃饭</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = '打游戏'
</script>
<script type="text/javascript" src='foo.js></script>
<div>睡觉</div>
</body>
</html>
复制代码