前言
在了解script标签添的 async 和 defer 属性之前,先来介绍一下浏览器的 load 与 DOMContentLoaded 事件。
1. load 与 DOMContentLoaded 的区别
load :
当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发load
事件。
它与DOMContentLoaded
不同,后者只要页面DOM加载完成就触发,无需等待依赖资源的加载。
DOMContentLoaded :
当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded
事件被触发,而无需等待样式表、图像和子框架的完全加载。另一个不同的事件 load
应该仅用于检测一个完全加载的页面。
注意: DOMContentLoaded 事件必须等待其所属 script 之前的样式表加载解析完成才会触发。
可以参考下面这段代码效果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#aaa{
width: 600px;
height: 300px;
background-color: pink;
}
#aaa>div{
width: 100px;
height: 100px;
background-color:black;
}
</style>
</head>
<script>
window.addEventListener("load", function(event) {
console.log("page is fully loaded 页面已完全加载");
});
document.addEventListener("DOMContentLoaded", function(event) {
console.log("DOM fully loaded and parsed DOM已完全加载和解析");
});
for(var i=0; i<1000000000; i++){
// 这个同步脚本将延迟DOM的解析。
// 所以DOMContentLoaded事件稍后将启动。
}
</script>
<body>
<div id="aaa">
<div></div>
<div></div>
<div></div>
</div>
</body>
</html>
来看看控制台中是怎么打印的👇
2. script 标签中 async 和 defer
async :
对于普通脚本,如果存在 async
属性,那么普通脚本会被并行请求,并尽快解析和执行。
对于模块脚本,如果存在 async
属性,那么脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。
该属性能够消除解析阻塞的 Javascript。解析阻塞的 Javascript 会导致浏览器必须加载并且执行脚本,之后才能继续解析。defer
在这一点上也有类似的作用。
这是个布尔属性:布尔属性的存在意味着 true 值,布尔属性的缺失意味着 false 值。
defer :
这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded
事件前执行。
有 defer
属性的脚本会阻止 DOMContentLoaded
事件,直到脚本被加载并且解析完成。
注意:如果缺少 src 属性(即内嵌脚本),该属性不应被使用,因为这种情况下它不起作用。
defer 属性对模块脚本没有作用 —— 他们默认 defer。
将上一段代码稍微做一点修改:
html 部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#aaa{
width: 600px;
height: 300px;
background-color: pink;
}
#aaa>div{
width: 100px;
height: 100px;
background-color:black;
}
</style>
</head>
<script src="js/async.js" async></script>
<body>
<div id="aaa">
<div></div>
<div></div>
<div></div>
</div>
</body>
<script>
console.log('内嵌js开始执行')
window.addEventListener("DOMContentLoaded", function(event) {
console.log("DOM fully loaded and parsed DOM已完全加载和解析");
});
window.addEventListener("load", function(event) {
console.log("page is fully loaded 页面已完全加载");
});
for(var i=0; i<1000000000; i++){
// 这个同步脚本将延迟DOM的解析。
// 所以DOMContentLoaded事件稍后将启动。
}
try{
console.log(a)
}
catch(err){
console.log(err)
}
console.log('内嵌js执行结束')
</script>
</html>
引入的 async.js:
console.log('外部async.js开始执行')
var a = 666;
for(var i=0; i<1000000000; i++){
// 这个同步脚本将延迟DOM的解析。
// 所以DOMContentLoaded事件稍后将启动。
}
console.log('外部async.js执行结束')
此时 script 标签中未添加任何属性,来看看控制台是怎么打印的👇
注意: 同步 JavaScript 会暂停 DOM 的解析。
可以看到,在控制台中正常打印出 a 值。script 代码也是从上往下执行。但是会出现短暂的白屏。
接下来给 script 标签添加 async
属性,再来看看效果🤔
可以看到页面加载过程中没有出现白屏现象✌
此时外部的脚本不会阻塞 DOM 的渲染,但程序抛出了一个错误,a 未定义,这是因为 async
的作用是让浏览器能够异步的加载脚本,不因加载脚本而阻塞页面的加载。不过不能保证 DOM 已经完全加载(也就是说,async 属性告诉浏览器先把文件下载下来,在“时机成熟”的时候再执行。异步脚本一定会在页面的 load 事件前执行,但可能会在 DOMContentLoaded
事件触发之前或之后执行。而且更加需要注意的是,标记为 async 的脚本并不保证按照指定他们的先后顺序执行。所以,确保各个异步脚本互不依赖非常重要)。
再来把 script 标签中的 async 改成 defer
,来看看效果。
defer 效果与 async 类似都不会阻塞 DOM 的渲染,但该脚本将在文档完成解析后,DOMContentLoaded
事件触发之前执行。
可以通过下面这张图来加深理解。
相同点 :
- 都是异步的加载外部 js 文件,加载时不会阻塞DOM渲染
- 都在页面 onload 之前执行
- 对于内嵌脚本无效
不同点 :
- async 属性的脚本无法确定是在
DOMContentLoaded
事件触发之前或之后执行。defer 属性的脚本在DOMContentLoaded
事件触发之前执行。 - async 属性的脚本并不一定会按照顺序执行(先加载完成先执行),defer 属性的脚本会按照顺序执行。
其实完全可以把 script 都写在 body 底部,这样既没有白屏问题,也没有执行顺序问题。
参考文章: defer和async的区别