HTML解析之DOMContentLoaded和onload

说在前面

在很久很久以前,我在封装自己的JQuery库时就使用过DOMContentLoaded,觉得这个知识点看看别的文章就行了,不过现在我想把它记下来。

JS代码与body标签的位置关系

一个HTML初学时会遇到的问题,一个html页面中js代码应该放到哪里?

<!--如果script标签在body前面-->
<head>
    ...
    <script>
        var box = document.getElementById('box');
        console.log(box)  //null
    </script>
</head>
<body>
    <div id="box"></div>
</body>
---------
<!--如果script标签在body后面(里面的最后也可以)-->
<head></head>
<body>
    <div id="box"></div>
    <script>
        var box = document.getElementById('box');
        console.log(box)  //div#box
    </script>
</body>

上面代码可以看出,如果js代码写在body标签前面,而且没用其他事件而直接获取dom的话是无法获取的。而js代码写在body标签的后面(里面的最后也可以)就可以获取dom。

实际上如果了解浏览器解析HTML规则就很清楚原因了,浏览器解析HTML由上往下依次执行,如果遇到<script>会阻塞解析,先执行该JS脚本(如果是外部JS文件还要先加载),执行结束后再接着往下解析,所以上面获取不到dom的原因是当时JS代码执行时页面DOM树尚未构建完成。具体分析往下看。

script标签的defer和async

从上面知道,浏览器解析HTML遇到script标签会阻塞。上面举例的JS代码都是内嵌在HTML中的,这样再解析到script时直接执行就行。但如果是引入外部JS文件的话会有一点不同,要先加载该JS文件,然后执行,然后在往下解析HTML。但script标签上还有两个常见属性defer和async

一般情况<script src="xxx.js">

当浏览器遇到 script 标签时,文档的解析将停止,并立即下载并执行脚本,脚本执行完毕后将继续解析文档。

defer <script defer src="xxx.js">

当浏览器遇到 script 标签时,文档的解析不会停止,JS文件的加载与文档解析并行(异步),待到文档解析DOM构建完成,脚本才会执行(在DOMContentLoaded事件触发之前)。

async <script async src="xxx.js">

当浏览器遇到 script 标签时,文档的解析不会停止,JS文件的加载与文档解析并行(异步),脚本下载完成后开始执行脚本,脚本执行时文档会停止解析。

看图(图片来源于网络)

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。

总结defer和async的区别:

  1. 加载时是一样的,相对于HTML解析是异步的。
  2. 不同的是执行时机,async在代码加载完之后会马上执行,并且执行时会阻塞HTML解析。而defer则要等到文档解析DOM构建完成,DOMContentLoaded事件触发之前执行
  3. async执行时机不确定性,要注意使用场景。

所以script标签加上defer属性,即使不用DOMContentLoaded或window.onload也可以获取操作DOM。

//index.js
var box = document.getElementById('box');
//可以获取到div
console.log(box);  //div#box
--------
<head>
    ...
    <script defer src="./index.js"></script>
</head>
<body>
    <div id="box"></div>
</body>

DOMContentLoaded和window.onload

DomContentLoaded

MDN解释当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。另一个不同的事件 load 应该仅用于检测一个完全加载的页面。 在使用 DOMContentLoaded 更加合适的情况下使用 load 是一个令人难以置信的流行的错误,所以要谨慎。

注意:DOMContentLoaded 事件必须等待其所属script之前的样式表加载解析完成才会触发

//index.css
#box{
    width:100px;
    height:100px;
    background: pink;
}
---------
//DOMContentLoaded前面引入样式表
<head>
    <link rel="stylesheet" href="./index.css"/>
    <script>
        function getStyle(el, attr) {
            return el.currentStyle ? el.currentStyle[attr] : getComputedStyle(el)[attr];
        }
        // DOMContentLoaded
        document.addEventListener('DOMContentLoaded', function(){
            var box = document.getElementById('box');
            //可以获取到样式表的样式
            console.log(getStyle(box, 'width'));    //100px
        })
    </script>
</head>
<body>
    <div id="box"></div>
</body>
--------
//DOMContentLoaded后面引入样式表
<head>
    <script>
        function getStyle(el, attr) {
            return el.currentStyle ? el.currentStyle[attr] : getComputedStyle(el)[attr];
        }
        // DOMContentLoaded
        document.addEventListener('DOMContentLoaded', function(){
            var box = document.getElementById('box');
            //可能无法获取到样式表的样式
            console.log(getStyle(box, 'width'));    //1350px或100px
        })
    </script>
    <link rel="stylesheet" href="./index.css"/>
</head>
<body>
    <div id="box"></div>
</body>

上面代码看出,在DOMContentLoaded后面引入样式表,DOMContentLoaded可能无法获取样式表里的样式,因为此时HTML解析完成,DOM树构建完成,但外部css文件可能还没加载完成

暂时得出结论:js代码应该放在样式表之后

window.onload

这个就没什么好说的,此时HTML文档解析完成,其他依赖资源也全部加载完成。

用document.readyState看一下各种情况下的HTML文档状态:

<script>
    window.onload = function(){
        console.log('window.onload', document.readyState);
    }
    document.addEventListener('DOMContentLoaded', function(){
        console.log('DOMCOntentLoaded', document.readyState);
    }, false);
    console.log(document.readyState);
</script>
//输出
//loading    文档加载中
//DOMCOntentLoaded interactive    文档与用户可以开始交互,可以操作DOM
//window.onload complete    一切完成

ready()简单实现

JQuery中有个$(document).ready(),下面就简单实现一下。

document.ready = function(fn){
    if(document.addEventListener){  //现代浏览器
        document.addEventListener('DOMContentLoaded', function(){
            document.removeEventListener('DOMContentLoaded', arguments.callee, false);
            fn();
        }, false);
    }else if(document.attachEvent){  //低版本IE
        document.attachEvent('onreadystatechange', function(){
            if(document.readyState === 'complete'){
                document.detachEvent('onreadystatechange', arguments.callee);   
                fn();
            }
        })
    }else{
        window.onload = fn;
    }
}

总结

HTML文档加载步骤:

  1. 由上往下解析HTML结构。
  2. 遇到src属性则发起请求加载资源,只有script会阻塞HTML解析,其他(css、img等)都不会影响HTML解析。
  3. script资源加载完,执行JS脚本。
  4. DOM树构建完成,触发DOMContentLoaded
  5. 其他css、img、iframe等资源如果还未加载完成继续加载。
  6. 页面加载完毕,触发window.onload

为什么要强调css放头部,js放尾部

因为css样式表是浏览器渲染页面的重要一环,应该尽早发起请求加载,毕竟也不会阻塞HTML解析。

而HTML解析遇到script会阻塞,所以应该放到后面,而不阻塞其他资源请求。虽然说还是要等script加载执行完成之后才会触发DOMContentLoaded,但现在很多现代浏览器为了更好地用户体验,能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。

参考文章    参考文章

发布了63 篇原创文章 · 获赞 18 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/samfung09/article/details/100753488