在网站页面中,通常需要引入外部js资源,然而外部的js资源可能导致DOM阻塞,影响页面加载速度。通过异步或者延迟执行js,可以做到引用外部js资源而不阻塞DOM的目的。用法是直接在script标签中使用defer和async,那么它们两个有什么区别呢?
从字面上来理解,defer是延迟,而async则是异步(Asynchronous)。w3c上这样解释defer和async:defer属性是一个布尔值的属性,当存在这个属性时,它指定的脚本将在页面解析之后执行1; async也是一个布尔值的属性,当存在这个属性时, 只要脚本是可用的它将异步执行2。更详细的可见火狐开发者中心对script标签的讲解。
概念的东西让人很难琢磨,下面是我对使用不同属性的外部引用标签的一个实验, 用php模拟一个外部javascript脚本:
<?php
header("Content-Type:text/javascript;charset=utf-8;");
sleep(2);
?>
console.log("This is the Script");
前台页面代码如下:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script src="./js.php" defer></script>
<!-- 对上面的引入脚本分别进行defer和async的测试-->
<body>
This is The Document.
</body>
</html>
下面的两张图片分别是使用defer和async的时间线:
使用defer的时间线
注意那条蓝色的时间线是DOMContentloaded时间,可以看到使用defer的DOMContentloaded事件是在JS脚本执行完之后,而使用async则是在html文档被解析完之后,相同之处是打印出”This is the Script”的时间一样。为什么会这样呢?
我们知道,如果javascript引入脚本既不使用defer也不使用async,整个页面是必须在引入脚本下载执行完毕之后才能继续被加载。而根据前面的概念,defer是延迟执行,async是异步执行。它们之间最大的区别是defer的执行时间是html文档被解析完成之后,而async的执行时间是不确定的,只要脚本被下载完毕,就会执行;它们之间的相同点是不会阻塞其它元素的加载。如果一个页面中有多个使用defer属性的外部脚本, 他们会按照顺序依次执行。
defer和async的应用
由于defer会在页面解析完成之后执行,所以当js脚本运行的时候,页面已经是解析完毕了的状态,这时候已经可以进行DOM操作, 而如果不使用defer,文档的状态不确定,所以不能进行操作。如上面的代码中,如果把js.php改成如下代码,打开前面页面,发现内容会被更改:
<?php
header("Content-Type:text/javascript;charset=utf-8;");
sleep(2);
?>
document.body.innerHTML="Body Content Has Been changed";
使用defer需要注意的是,由于defer的执行时机是页面解析完之后,含有defer的外部脚本执行时间是优先于内嵌脚本的,如下,内嵌脚本中放入一个DOMContentloaded的事件监听:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<script>
document.addEventListener("DOMContentLoaded",function(){
console.log("内部脚本,DOMContentloaded发生!");
},false);
</script>>
<script src="./js.js" defer></script>
<!--外部脚本内容: console.log("外部脚本, 加载完毕!");-->
<body>
This is The Document.
</body>
</html>
发现外部脚本内容先被打印出来。
而对于使用async的外部脚本来说,是在javascript文件被完全加载之后马上执行, 所以它可能会在DOM加载好之前被执行,也可能会在之后执行。这里的问题就是,如果异步加载的JS在DOM准备好之后执行,那么操作DOM的动作势必执行不了,如果在DOM准备好之前执行,则无法监听DOMContentloaded事件, 而依赖DOMContentloaded的事件回调就无法启动。例如一个HTML页面加入如下异步脚本代码:
<?php
header("Content-Type:text/javascript");
sleep(5);
?>
document.addEventListener("load",function(){
console.log('DOMContentLoaded Event');
},false);
发现”DOMContentLoaded Event”无法打印,而去掉async属性后,就会正常打印”DOMContentLoaded Event”, 这说明在异步加载后,外部脚本执行的时候,DOMContentLoaded事件已经过去。
综上,defer和async最大的区别是执行时间的不同,defer实在DOM解析完毕之后马上执行,而async是在脚本下载完毕之后马上执行。执行时机的不同造成最终的效果各不相同,在实际中要小心对待。