测试的缘由
很早就接触了script
标签中的defer
和async
这两个属性,当时也仅限知道他们的作用是起到延迟脚本而达到避免阻塞脚本的作用。当时并未对这两个属性进行深入的研究,直到我学习seajs
和RequireJS
这两个模块加载器中或多或少涉及了这两标签的使用。seajs通过异步的ajax请求脚本并执行,而RequireJS则是将依赖的脚本通过 head.appendchild(script)并设置延迟属性来达到异步加载脚本的目的。
不行请看:
而且对它的网络瀑布流进行分析时发现一个奇怪的现象,有时强制刷新时,带async
的脚本会延迟到window的onload
事件执行完毕后才执行。这样就会很容易导致一个问题,在这些脚本中绑定window的onload
事件是失败的。因为window的onlaod
执行先于这些延迟的脚本。如图所示:
图中的蓝线表示DOM执行完毕的时间轴,而红线表示window的load
执行完毕的时间轴。你会发现那些通过RequireJS加载的模块脚本会在load
事件执行完毕后执行。而且更有意思的是,这种情况也是随机出现的。
这完全颠覆了我对defer
和async
的理解,所以有必要对其进行测试。
defer和async定义
在进行测试前有必要介绍标准对其进行的定义,同时进行兼容性测试。首先来看看标准的定义吧。
defer
This Boolean attribute is set to indicate to a browser that the script
is meant to be executed after the document has been parsed. Since this
feature hasn't yet been implemented by all other major browsers,
authors should not assume that the script’s execution will actually be
deferred. The defer attribute shouldn't be used on scripts that don't
have the src attribute. Since Gecko 1.9.2, the defer attribute is
async
Set this Boolean attribute to indicate that the browser should, if possible, >execute the script asynchronously. It has no effect on inline scripts (i.e., >scripts that don't have the src attribute).
ignored on scripts that don't have the src attribute. However, in
Gecko 1.9.1 even inline scripts are deferred if the defer attribute is
set.
英文很渣,看不懂怎么办,没事,看看《JavaScript高级程序设计3》(第二章第13页)是怎么解释的。
defer
HTML4.01为<script>标签定义了defer属性。这个属性的用途是标明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后在执行。因此,在<script>元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。
在HTML5规范中要求脚本按照它们的出现的先后顺序执行,而且脚本会优先于DOMContentLoaded事件执行。在现实当中,延迟的脚本并不一定按照顺序执行,也不一定会在DOMContentLoaded事件触发执行,因此最好只包含一个延迟脚本。
async
HMTL5为<script>标签定义了async属性。这个属性与defer属性类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。
异步脚本一定会在页面的load事件前执行,但可能会在DOMContentLoad事件触发之前或之后执行。
兼容性
官方给出的兼容性测试
不过,你懂得,浏览器更新迭代太快了,这个版本支持,下个版本可能就不支持或者支持的不够完整,为了测试我机器上浏览器上的支持情况,你可以引入检查的脚本,链接为:http://segmentfault.com/blog/liangyi/1190000002434713#articleHeader3
通过运行得到本机所安装浏览器支持的情况:
IE6、IE7、IE8、IE9、Chrome39、FF34都支持defer
属性。
IE6、IE7、IE8、IE9不支持async
属性,但是Chrome39、FF34支持。
测试环境部署
在得到兼容性的报表后,我们就可以进行测试了,但是首先还得交代测试用的Demo示例。本示例用到两个测试文件,代码分别如下:
demo.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>script async defer</title>
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script type="text/javascript">
$(window).on("load", function (e) {
console.log("WindowLoaded");
});
$(document).ready(function (e) {
console.log('DOMContentLoaded');
});
</script>
<script type="text/javascript" src="http://127.0.0.1:8081/test1"></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test2"></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test3"></script>
</head>
<body>
<h1>测试延迟和异步加载脚本</h1>
<script>
console.log("Inline script Loaded");
</script>
</body>
server.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
if (req.url == '/test1') {
setTimeout(function() {
res.end("console.log('test1');");
}, 1000 * 5);
} else if (req.url == '/test2') {
setTimeout(function() {
res.end("console.log('test2');");
}, 1000 * 3);
} else {
res.end("console.log('test3');");
}
// res.end('Hello World\n');
}).listen(8081, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8081/');
这里用Node做服务器。请求的连接根据URL不同来请求不同的脚本资源。你也可以用其他的服务器,比Apache、IIS等。或着更直接,将请求的三个脚本资源放在本地。不过相比Node是最简单的部署方式,而且我使用WebStorm可以直接运行和调式Node脚本。
PS:小技巧:如果你使用WebStorm,可以直接点击右上角的浏览器来运行demo.html,它会启动内置的浏览器来运行页面,这样模拟更真实。
测试用列
不包含defer和async的基准测试,demo.html的代码如下所示:
<script type="text/javascript" src="http://127.0.0.1:8081/test1"></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test2"></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test3"></script>
控制台的输出情况(IE6-9,FF34、Chrome39):
test1 -> test2 -> test3-> ->Inline script Loaded -> DOMContentLoaded -> WindowLoaded
总结:这输出的结果和我预想的结果完全吻合,因为在不加任务延迟或者异步的情况下,浏览器遇到Script标签就会下载并执行,而且执行的顺序是同步的,所以尽管test3下载最快,但是会在最后执行。另外加载和执行这些脚本会阻塞页面的渲染,所以会白屏一段时间,而且Inline script Loaded的输出是后于test3输出的。
只包含defer,不包含async的测试,demo.html的改动如下:
<script type="text/javascript" src="http://127.0.0.1:8081/test1" defer></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test2" defer></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test3" defer></script>
在IE6-8输出的情况如下所示:
Inline script Loaded -> DOMContentLoaded -> test1 -> test2 -> test3 -> WindowLoaded
而在IE9、Chrome39、FF34中输出的情况如下所示:
Inline script Loaded -> test1 -> test2 -> test3-> DOMContentLoaded -> WindowLoaded
总结:跟上面的结果做对比,你会发现页面中标题的文本一下就渲染出来了,而且body元素中的script标签脚本是由于其他脚本执行结束的。虽然并不是所有的浏览器中的带defer的脚本等DOMContentLoaded结束后才执行,但是它们都是在body中的元素渲染完毕后才执行的。也就是说defer确实做到了没有阻塞页面的渲染。
只包含async,不包含defer的测试,demo.html的改动如下:
<script type="text/javascript" src="http://127.0.0.1:8081/test1" async></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test2" async></script>
<script type="text/javascript" src="http://127.0.0.1:8081/test3" async></script>
由于我的机器上只有FF和Chrome支持async
,所以只对上述的浏览器做了测试,测试结果如下所示:
chrome控制台输出的结果为:
Inline script Loaded -> DOMContentLoaded -> test3 -> test2 -> test1
WindowLoaded
FF控制台输出的结果为:
Inline script Loaded -> test3 -> DOMContentLoaded -> -> test2 -> test1
WindowLoaded
总结:首先他们都没有阻塞页面的渲染,在请求的一瞬间,页面上的标题文本,立即渲染出来了,通过Inline script Loaded最早被输出我们也可以知道。带async属性的外链脚本,一下载完成,就被执行了,所以你看到的顺序是test3 > test2 > test1。他们的顺序不管在哪个浏览器中出现的顺序都是一样的。但是为什么在FF中,test3执行的比DOMContentLoaded
早,我就不太明白,这点希望有高人指点。async属性也做到了没有阻塞页面渲染。但是执行的机制却和defer打不相同。
帮助文档
本文,主要是通过测试的目的来验证defer和async的加载机制,更多细节可以参考下面的链接:
script的defer和async – 携程UED
引用JavaScript文件时的两个属性defer和async
async vs defer attributes
defer和async的区别
script关于async与defer属性的测试 - 测试文件参考这位大牛
同时,文章写的有些长处,如果文章有什么出入,希望大神不吝指点。