当我们在浏览器地址栏输入一个合法的url时,浏览器首先进行DNS域名解析,拿到服务器IP地址后,浏览器给服务器发送GET请求,等到服务器正常返回后浏览器开始下载并解析html。这里仅总结浏览器解析html的过程。
html页面主要由dom、css、javascript等部分构成,其中css和javascript既能内联也能以脚本的形式引入,当然html中还可能引入img、iframe等其他资源。其实所有的这些资源也是以dom标签的形式嵌入在html页面中的,因此本篇总结说的html解析过程就是dom的解析过程。
1 dom解析过程
整个dom的解析过程是顺序,并且渐进式的。
顺序指的是从第一行开始,一行一行依次解析;渐进式则指得是浏览器会迫不及待的将解析完成的部分显示出来,如果我们做下面这个实验会发现,在断点处第一个div已经在浏览器渲染出来了:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
first div
</div>
<script>
debugger
</script>
<div>
second div
</div>
</body>
</html>
既然dom是从第一行按顺序解析,那么我们怎么判断dom何时解析完成呢?这个问题应该经常会在面试中问到,比如一般会问:
window.onload和DOMContentLoaded有什么区别?
其实就是想看看是不是明白dom树何时构建完成,这个问题确实很重要,尤其是对于几年前的jquery技术栈来说,因为我们使用javascript操作dom或者给dom绑定事件有个前提条件就是需要dom树已经创建完成。整个html页面的dom解析完成时,dom树也就构建完成了。dom树构建完成后document对象会派发事件DOMContentLoaded来通知dom树已构建完成。
html从第一行开始解析,遇到外联资源(外联css、外联javascript、image、iframe等)就会请求对应资源,那么请求过程是否会阻塞dom的解析过程呢?答案是看情况,有的资源会,有的资源不会。下面按是否会阻塞页面解析分为两类:阻塞型与非阻塞型,注意这里区分两类资源的标志是document对象派发DOMContentLoaded事件的时间点,认为派发DOMContentLoaded事件才表示dom树构建完成。
1.1 阻塞型
会阻塞dom解析的资源主要包括:
- 内联css
- 内联javascript
- 外联普通javascript
- 外联defer javascript
- javascript标签之前的外联css
外联javascript可以用async与defer标示,因此这里分为了三类:外联普通javascript,外联defer javascript、外联async javascript,这几类外联javascript本篇后面有详细介绍。 dom解析过程中遇到外联普通javascript会暂停解析,请求拿到javascript并执行,然后继续解析dom树。
对于外联defer javascript这里重点说明下为什么也归于阻塞型。前面也说了,这里以document对象派发DOMContentLoaded事件来标识dom树构建完成,而defer javascript是在该事件派发之前请求并执行的,因此也归类于阻塞型,但是需要知道,defer的javascript实际上是在dom树构建完成与派发DOMContentLoaded事件之间请求并执行的,不过如果换个思路理解,<script>本身也是dom的一部分也就不难理解为什么defer的javascript会在DOMContentLoaded派发之前执行了。
另外需要注意的是javascript标签之前的外联css。其实按说css资源是不应该阻塞dom树的构建过程的,毕竟css只影响dom样式,不影响dom结构,MDN上也是这么解释的:
The
DOMContentLoadedevent is fired when the initial HTML document has been completely loaded and parsed, without waiting forstylesheets, images, and subframes to finish loading.
但是实际情况是dom树的构建受javascript的阻塞,而javascript执行时又可能会使用类似Window.getComputedStyle()之类的API来获取dom样式,比如:
const para = document.querySelector('p');
const compStyles = window.getComputedStyle(para);
因此浏览器一般会在遇到<script>标签时将该标签之前的外联css请求并执行完成。但是注意这里加了一个前提条件就是javascript标签之前的外联css,就是表示被javascript执行依赖的外联css。这个容易忽略的点这篇文章也有说明,推荐阅读。
这些阻塞型的资源请求并执行完之后dom树的解析便完成了,这时document对象就会派发DOMContentLoaded事件,表示dom树构建完成。
1.2 非阻塞型
不阻塞dom解析的资源主要包括:
- javascript标签之后的外联css
- image
- iframe
- 外联async javascript
dom树解析完成之后会派发DOMContentLoaded事件,对于外联css资源来说分为两类,一类是位于<script>标签之前,一类是位于<script>标签之后。位于<script>标签之后的外联css是不阻塞dom树的解析的。外联css对dom树解析过程的影响这里有一篇非常好的文章介绍:DOMContentLoaded and stylesheets,推荐阅读。
DOMContentLoaded事件用来标识dom树构建完成,那如何判断另外这些非阻塞型的资源加载完成呢?答案是window.onload。由于该事件派发的过晚,因此一般情况下我们用不着,而更多的是用DOMContentLoaded来尽早的的操作dom。
另外还有image、iframe以及外联async javascript也不会阻塞dom树的构建。这里外联async javascript又是什么呢?下一节整体介绍下外联javascript。
2 外联javascript加载过程
html页面中可以引入内联javascript,也可以引入外联javascript,外联javascript又分为:
- 外联普通javascript
<script src="indx.js"></script>
- 外联defer javascript
<script defer src="indx.js"></script>
- 外联async javascript
<script async src="indx.js"></script>
其中第一种就是外联普通javascript,会阻塞html的解析,html解析过程中每遇到这种<script>标签就会请求并执行,如下图所示,绿色表示html解析;灰色表示html解析暂停;蓝色表示外联javascript加载;粉色表示javascript执行。
是 外联普通javascript的加载执行过程如下:
第二种 外联defer javascript稍有不同, html解析过程中遇到此类 <script>标签不阻塞解析,而是会暂存到一个队列中,等整个 html解析完成后再按队列的顺序请求并执行 javascript,但是这种 外联defer javascript全部加载并执行完成后才会派发 DOMContentLoaded事件, 外联defer javascript的加载执行过程如下:
第三种 外联async javascript则不阻塞 html的解析过程,注意这里是说的脚本的 下载过程不阻塞 html解析,如果下载完成后 html还没解析完成,则会暂停 html解析,先执行完成下载后的 javascript代码再继续解析 html,过程如下:
但是如果 html已经解析完毕, 外联async javascript还未下载完成,则不阻塞 DOMContentLoaded事件的派发。因此 外联async javascript很有可能来不及监听 DOMContentLoaded事件,比如 stackoverflow上的 这个问题。
说明下,这几个图引用自这里。
3 DOMContentLoaded兼容性问题
DOMContentLoaded最开始由firefox提出,其他浏览器觉得非常有用也相继开始支持,但是特性却稍有不同,比如opera中javascript的执行并不等待外联css的加载。直到HTML5出来后将DOMContentLoaded标准化,依照HTML5标准,javascript脚本执行前,出现在当前<script>之前的<link rel="stylesheet">必须完全载入。
那么在所有浏览器标准化之前怎么解决DOMContentLoaded的兼容性问题呢?可以参考jQuery中.ready()方法的实现,对于该方法的源码分析网上已经一大堆了,这里就不做分析了,直接说下原理。其实是就是用了MDN: DOMContentLoaded中介绍的兼容性方法,ie9才开始支持DOMContentedLoaded,ie8环境可以通过检测document.readystate状态来确认dom树是否构建完成。document.readystate包括3种状态:
- loading – html文档加载中
- interactive – html文档加载并解析完成,但是图片等资源还未完成加载,相当于
DOMContentLoaded - complete – 所有资源加载完成,相当于
window onload
因此我们通过判断document.readystate的状态为interactive来模拟DOMContentLoaded时间点。但是这里需要注意一点,以.ready()方法为例,我们可能在下面这几个地方调用:
- 内联javasctipt
- 外联普通javascript
- 外联defer javascript
- 外联async javascript
其中3三个地方直接判断document.readystate肯定是loading状态,只有外联async javascript可能出现document.readystate为interactive或completed的状态,因为外联async javascript是不阻塞dom解析的,因此为了完全覆盖前面的4种情况,需要监听document.readystate的变化:
if (document.readystate === 'interactive'
|| document.readystate === 'complete') {
// 调用ready回调函数
} else {
document.onreadystatechange = function () {
if (document.readystate === 'interative') {
// 调用ready回调函数
}
}
}
4 引用
主要参考了以下文章,推荐阅读:
1、 Page lifecycle: DOMContentLoaded, load, beforeunload, unload
2、 DOMContentLoaded and stylesheets
3、 script标签: async vs defer attributes
4、 MDN: DOMContentLoaded
5、 MDN: readystatechange
6、 Replace jQuery’s Ready() with Plain JavaScript