# 22 | DOM树:JavaScript是如何影响DOM树构建的? 在[上一篇文章](https://time.geekbang.org/column/article/138844)中,我们通过开发者工具中的网络面板,介绍了网络请求过程的几种**性能指标**以及对页面加载的影响。 而在渲染流水线中,后面的步骤都直接或者间接地依赖于DOM结构,所以本文我们就继续沿着网络数据流路径来**介绍DOM树是怎么生成的**。然后再基于DOM树的解析流程介绍两块内容:第一个是在解析过程中遇到JavaScript脚本,DOM解析器是如何处理的?第二个是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解析器是怎么工作的。 在开始介绍HTML解析器之前,我要先解释一个大家在留言区问到过好多次的问题:**HTML解析器是等整个HTML文档加载完成之后开始解析的,还是随着HTML文档边加载边解析的?** 在这里我统一解答下,HTML解析器并不是等整个文档加载完成之后再解析的,而是**网络进程加载了多少数据,HTML解析器便解析多少数据**。 那详细的流程是怎样的呢?网络进程接收到响应头之后,会根据响应头中的content-type字段来判断文件的类型,比如content-type的值是“text/html”,那么浏览器就会判断这是一个HTML类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,**网络进程和渲染进程之间会建立一个共享数据的管道**,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给HTML解析器。你可以把这个管道想象成一个“水管”,网络进程接收到的字节流像水一样倒进这个“水管”,而“水管”的另外一端是渲染进程的HTML解析器,它会动态接收字节流,并将其解析为DOM。 解答完这个问题之后,接下来我们就可以来详细聊聊DOM的具体生成流程了。 前面我们说过代码从网络传输过来是字节流的形式,那么后续字节流是如何转换为DOM的呢?你可以参考下图: ![](https://static001.geekbang.org/resource/image/1b/8c/1bfcd419acf6402c20ffc1a5b1909d8c.png) 字节流转换为DOM 从图中你可以看出,字节流转换为DOM需要三个阶段。 **第一个阶段,通过分词器将字节流转换为Token。** 前面[《14 | 编译器和解释器:V8是如何执行一段JavaScript代码的?》](https://time.geekbang.org/column/article/131887)文章中我们介绍过,V8编译JavaScript过程中的第一步是做词法分析,将JavaScript先分解为一个个Token。解析HTML也是一样的,需要通过分词器先将字节流转换为一个个Token,分为Tag Token和文本Token。上述HTML代码通过词法分析生成的Token如下所示: ![](https://static001.geekbang.org/resource/image/b1/ac/b16d2fbb77e12e376ac0d7edec20ceac.png) 生成的Token示意图 由图可以看出,Tag Token又分StartTag 和 EndTag,比如`
`就是StartTag ,`就是EndTag`,分别对于图中的蓝色和红色块,文本Token对应的绿色块。 **至于后续的第二个和第三个阶段是同步进行的,需要将Token解析为DOM节点,并将DOM节点添加到DOM树中。** HTML解析器维护了一个**Token栈结构**,该Token栈主要用来计算节点之间的父子关系,在第一个阶段中生成的Token会被按照顺序压到这个栈中。具体的处理规则如下所示: * 如果压入到栈中的是**StartTag Token**,HTML解析器会为该Token创建一个DOM节点,然后将该节点加入到DOM树中,它的父节点就是栈中相邻的那个元素生成的节点。 * 如果分词器解析出来是**文本Token**,那么会生成一个文本节点,然后将该节点加入到DOM树中,文本Token是不需要压入到栈中,它的父节点就是当前栈顶Token所对应的DOM节点。 * 如果分词器解析出来的是**EndTag标签**,比如是EndTag div,HTML解析器会查看Token栈顶的元素是否是StarTag div,如果是,就将StartTag div从栈中弹出,表示该div元素解析完成。 通过分词器产生的新Token就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。 为了更加直观地理解整个过程,下面我们结合一段HTML代码(如下),来一步步分析DOM树的生成过程。 ```