1、 defer
该属性用来设置<script>
引用的外部脚本在文档完成解析后,触发DOMContentLoaded
事件之前执行,属性值为true(defer)/false
,默认为false
,设置为true
时可简写为只写defer
。
浏览器在解析 HTML 文档时,如果遇到 <script>
,便会停下对 HTML 文档的解析,转而去处理脚本。如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后再执行。等到脚本执行结束后,浏览器才会继续解析 HTML 文档。
但如果页面中存在设置了defer="defer"
属性并且是通过src
引用的外部脚本的<script>
,则该脚本不会阻碍文档的解析,只会在文档解析的同时去加载脚本信息,当文档解析完成后,暂时阻止DOMContentLoaded
事件的执行,去执行已经加载好的脚本信息,如果此时脚本还未加载完成,则会等待脚本加载完成后,再执行该脚本。执行完成后,再触发DOMContentLoaded
事件。
如果页面上存在多个设置了defer="defer"
属性并通过src
引用的外部脚本的<script>
,浏览器会在文档解析的同时并行的加载这些脚本。当HTML文档解析完成后,会先检测这些脚本是否全部加载完成,若全部加载完成,则会按照它们在HTML文档中的先后顺序从上到下依次执行;若是没有全部加载完成,则会先等待其下载完成后,再按照它们在HTML文档中的先后顺序从上到下依次执行。全部脚本执行结束后,再触发DOMContentLoaded
事件。
如果不是通过src
引用的外部脚本,而是直接在<script>
标签书写内嵌脚本,则设置defer
属性无效。除此之外,defer
属性对模块脚本也不起作用,因为模块脚本默认defer
。
如果我们引用的外部脚本1.js
需要依赖其他脚本2.js
中的信息,也就是需要确保脚本1.js
在执行时,脚本2.js
已经加载并执行完成,此时我们就可以通过设置脚本1.js
的defer="defer"
来实现这种效果。
在Vue项目中,最终打包生成的js文件,在index.html中引用时,都带有defer="defer"
属性。
案例代码:
// 正常加载顺序从上到下
<script src="./js/test.js"></script> // console.log('这是普通外链的js文件');
// 设置defer属性
<script defer src="./js/test1.js"></script> // console.log('这是设置了defer属性的外链的js文件');
// 内嵌脚本
<script>
console.log('这是内嵌的JS脚本');
document.addEventListener('DOMContentLoaded', function () {
console.log('这是页面的DOMContentLoaded事件');
});
</script>
执行结果:
2、 async
该属性用来设置<script>
引用的外部脚本异步加载,不阻塞HTML文档的解析,属性值为true(async)/false
,默认为false
,设置为true
时可简写为只写async
。
对于普通脚本,如果设置该属性,那么该脚本将会并行加载,不阻碍HTML文档的解析,当加载完成后,如果此时文档还没解析完成,则会终止解析,先执行该脚本,执行结束后再继续解析;如果此时文档已经解析完成了,那就是立即执行该脚本。所以设置该属性的外部脚本的执行时机并不确定,根据网络状况可能会在DOMContentLoaded
事件之前,也有可能会在DOMContentLoaded
事件之后。所以在脚本里面可能会获取不到在 HTML 中定义的元素,因为此时元素可能还没有被解析。
对于模块脚本来说,如果设置该属性,那么该脚本及其所有依赖将会并行加载,不阻碍HTML文档的解析,加载完成后逻辑与普通脚本相同。
如果页面上存在多个设置了async"
属性并通过src
引用的外部脚本的<script>
,那么浏览器会在解析HTML文档的同时去并行加载这些外部脚本,执行顺序由加载完成顺序决定,谁先加载完成,谁就先执行。
我们在js中通过document.createElement("script");
创建的<script>
标签,如果我们通过给其src
属性设置引用脚本,则这个创建的脚本默认为async
异步的。如果我们通过其textContent
设置内嵌脚本信息,则这个创建的脚本默认为async
同步的。
console.log('这是内嵌的JS脚本');
document.addEventListener('DOMContentLoaded', function () {
console.log('这是页面的DOMContentLoaded事件');
});
// 创建异步脚本
const script = document.createElement("script");
script.src = "./js/test.js";
document.body.appendChild(script);
// 创建同步脚本
const script = document.createElement("script");
script.textContent = "console.log('这是创建出来的同步脚本')";
document.body.appendChild(script);
执行结果:
生成的页面DOM结构:
3、defer和async
如果页面上同时存在设置了defer
的<script>
标签和设置了async
的<script>
标签,那么这两个脚本之间的执行顺序是不确定的,两者几乎是同时开始加载的,根据加载完成时机,有下面几种情况:
① 如果async
脚本先加载完,defer
脚本后加载完,则先执行async
脚本。
② 如果defer
脚本先加载完,async
脚本在文档已经解析完成并且DOMContentLoaded
事件之后加载完,则是defer
脚本先执行的。
③ 如果很碰巧的在文档解析完成后,DOMContentLoaded
事件之前,async
的异步脚本加载完成了,并且defer
脚本也已经加载完成了,那此时还是async
脚本先执行,然后再执行defer
脚本,因为async
的优先级高于defer
。
案例代码:
// 正常加载顺序从上到下
// 设置async属性
<script async src="./js/test.js"></script> // console.log('这是async外链的js文件');
// 设置defer属性
<script defer="defer" src="./js/test1.js"></script> // console.log('这是设置了defer属性的外链的js文件');
// 内嵌脚本
<script>
console.log('这是内嵌的JS脚本');
document.addEventListener('DOMContentLoaded', function () {
console.log('这是页面的DOMContentLoaded事件');
});
</script>
执行结果1:async
脚本先执行
执行结果2:defer
脚本先执行
如果一个<script>
标签同时使用了defer
和async
属性,那此时浏览器将会以async
的特性去加载脚本,因为async
的优先级高于defer
。
案例代码:
// 正常加载顺序从上到下
// 同时设置async和defer属性
<script async defer src="./js/test.js"></script> // console.log('这是同时设置了async和defer的脚本');
// 内嵌脚本
<script>
console.log('这是内嵌的JS脚本');
document.addEventListener('DOMContentLoaded', function () {
console.log('这是页面的DOMContentLoaded事件');
});
</script>
执行结果:
4、不同scritp的加载和执行时机
参考文档:
https://blog.csdn.net/mrlmx/article/details/127581208
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script
https://developer.mozilla.org/zh-CN/docs/Games/Techniques/Async_scripts