什么是DOM
- html文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是DOM。简言之,DOM是表述HTML的内部数据结构,它会将web页面和js脚本连接起来,并过滤一些不安全的内容。
- 在渲染引擎中,DOM有三个层面的作用:
- 从页面的视角看,DOM是生成页面的基础数据结构
- 从js脚本视角看,DOM提供给js脚本操作的接口,通过这套接口,js可以对DOM结构进行访问,从而改变文档结构、样式和内容。
- 从安全视角来看,DOM是一道安全防护线,一些不安全的内容,在DOM解析阶段就被拒之门外。
- DOM的解析是随着HTML边加载边解析的:
- 网路进程接收到响应头之后,会根据响应头里的content-type字段来判断文件的类型,比如HTML类型的文件content-type就是'text/html',然后为该请求选择或创建一个渲染进程。渲染进程准备好后,网络进程和渲染进程之间就会建立一个共享数据的管道,网络进程接收到数据(字节流)之后就往这个管理到放,而渲染进程则从管道的另一端不断第读取数据,并同时将读取的数据给HTML解析器,解析器就会把数据解析为DOM。
DOM树如何生成?

- 第一阶段:通过分词器将字节流转换成Token,分别是Tag Token和文本Token,Tag Token还分为StartTag和EndTag。

- 第二阶段是将Token解析为DOM节点,第三阶段是将DOM节点添加到DOM树中。第二第三阶段是同步进行的。
HTML解析器维护了一个Token栈结构。遇到StartTag Token就入栈,遇到文本Token就会生成一个文本节点,然后将该节点放到DOM树中,不需要压栈。遇到EndTag Token就会查看栈顶元素是否是StartTag,如果是,就将StartTag从栈中弹出,表示该div元素解析完成。
解析器开始工作时,会默认创建一个跟为document的空DOM结构,同时将一个StartTag document的Token压入栈底。

在解析过程中遇到js脚本,DOM解析器是如何处理的
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>
- HTML解析器遇到js脚本,会暂停DOM的解析,JavaScript引擎介入,并执行script中的这段脚本,脚本执行完后,DOM节点内容已经被修改了,HTML解析器恢复解析过程,继续解析后续的内容,直至生成最终的DOM。
DOM解析器是如何处理跨站点资源
<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>
- HTML解析器知道到script标签时,暂停整个DOM的解析,先下载js文件,然后再执行js代码。js文件下载过程会阻塞DOM解析。
Chrome的优化
- 预解析操作:当渲染引擎收到字节流的时候,会开启一个预解析线程,用来分析HTML文件中包含的JavaScript、css等相关文件,解析到相关文件之后,预解析器会提前下载这些文件。
将js脚本设置为异步加载
- async:文件加载完后马上执行,
- defer;文件加载完后,在DOMContentLoaded事件前才执行。
CSSOM
- 渲染引擎在遇到js脚本时,不管脚本是否操纵了CSSOM,都会执行css文件下载,解析操作,再执行js脚本,所以说,js脚本时依赖样式表的,这其实也是一个阻塞过程。
- js会阻塞DOM生成,而样式文件又会阻塞js的执行,所以在实际的工程中需要重点关注js文件和样式表文件,使用不当会影响页面性能。
参考:
JavaScript 是如何影响 DOM 生成的