DOM树是什么
众所周知,现代浏览器采取的是客户端+服务端的形式,客户端向服务端发送请求,服务端响应请求,将请求的内容放在响应主体里面。但浏览器中的网络进程收到从服务端发送过来的数据一般是 HTML 文件字节流,这是无法被浏览器中的渲染引擎直接读取响应的,所以需要对这个HTML文件进行转换,将其转换为渲染引擎能够理解的结构,就比如我们写的高级语言都需要通过编译/解释后才能被CPU来执行。渲染引擎能够理解的这个结构就是DOM树。
在渲染引擎中,DOM的作用
- DOM 是生成页面的基础数据结构
- DOM向JS提供了接口,使JS可以对DOM结构进行访问,从而动态改变DOM
- DOM是一道安全防护,一些不安全的内容在 DOM 解析阶段就被拒绝了
DOM 树是怎样生成的?
在渲染引擎内部,存在一个 HTLM 解析器(html parser)的模块,专门负责将HTML字节流转换为DOM结构,但注意,HTML解析器采取的是边加载数据边解析的方式。解析详细流程如下
- 网络进程在接收到响应头后,根据响应头的 content-type 字段判断文件类型,若是 text/html,浏览器会判断这是一个 html 类型的文件
- 浏览器接着就会为该文件准备一个渲染进程,可能是选择已有的,也可能是fork一个新的
- 渲染进程准备好之后,网络进程和渲染进程之间就会建立起一个共享数据的管道,用来传输HTML字节流,同时渲染进程也会边接受,边加载。
关于HTML字节流如何转换成DOM树,则涉及到编译原理的内容。
当HTML解析器遇见 script标签内嵌JS代码时
因为 HTML 有 script标签,且script标签几乎可以插在任何地方,当HTML解析器遇到script标签时,它会暂停自己的工作,将控制权交给JS引擎,并执行script标签中的JS代码,当JS代码执行完成后,控制权交还给HTML解析器,继续解析,直至生成最终的DOM树
当HTML解析器遇见外部加载JS代码的script标签时
当 HTML 解析器执行到script标签时,还是会暂停自己的工作,但是这里因为没有JS代码,所以无法执行,需要先去下载该代码,下载完成后再来执行JS代码,后面的流程就跟上面基本一样了。但是浏览器这样的抉择无疑会带来性能问题,即JS代码的下载,阻塞了DOM树的解析,阻塞了页面的生成。所以现代浏览器对于这一块做了大量的优化,Chrome采取的最主要的优化就是预解析操作。
- 当渲染引擎接受到字节流之后,会开启一个预解析线程,用来分析HTML文件中的JS文件、CSS等相关文件
- 解析到这些文件之后,该线程会提前下载这些文件,这样就能够缩减DOM解析的时间
同样,开发者也可以采取设置异步加载JS的方式来优化,通过 async 或者 defer 来设置,二者的区别在于
- async 标志的脚本文件一旦加载完成就会立刻执行
- defer 标记的需要在 DOMCotentLoaded 事件之前执行