defer和async特性相信是很多JavaScript开发者"熟悉而又不熟悉"的两个特性,从字面上来看,二者的功能很好理解,分别是"延迟脚本"和"异步脚本"的作用。然而,以defer为例,一些细节问题可能开发者却并不一定熟悉,比如:有了defer特性的脚本会延迟到什么时候执行;内部脚本和外部脚本是不是都能够支持defer;defer后的脚本除了会延迟执行之外,还有哪些特殊的地方等等。本文结合已有的一些文章以及MDN文档中对两个特性的阐述,对defer和async进行更全面的研究和总结,希望能够帮助开发者更好地掌握这两个特性。
1 引言
在《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中我们提到过,JavaScript代码的执行会阻塞页面的解析渲染以及其他资源的下载,当然由于JavaScript是单线程语言,那就意味着在正常情况下,一个页面中的JavaScript代码只能按顺序从上到下执行,当然,正如《浏览器环境下JavaScript脚本加载与执行探析之代码执行顺序》中我们分析的,在某些情况下,比如通过document.write进入脚本或者通过动态脚本技术引入脚本时,JavaScript代码的执行顺序不一定严格按照从上到下的顺序,而defer和async也是我们所说的"非正常的情况"。
我们经常会说JavaScript的执行具有阻塞性,而在实际的开发中,我们通常最关心的阻塞,同时也是最影响用户体验的阻塞应该是以下几个方面:
[1]页面解析和渲染的阻塞
[2]我们写的页面初始化脚本(一般是监听DOMContentLoaded事件所绑定的脚本,这部分脚本是我们希望最先执行的脚本,因为我们会把和用户交互最相关的代码写在这里)
[3]页面外部资源下载的阻塞(比如图片)
如果我们有一个耗时的脚本操作,而这段脚本又阻塞了上面我们提到的这三个地方,那么这个网页的性能或者用户体验就非常差了。
defer和async这两个特性的初衷也是希望能够解决或者缓解阻塞对于页面体验的影响,下面我们就来分析一下这两个特性,我们主要从以下几个方面来全方位了解这两个特性:
[1]延迟或异步的脚本的执行时机是什么时候?对于页面的阻塞情况如何?
[2]内部脚本和外部脚本是否都能够实现延迟或异步?
[3]浏览器对这两个特性的支持情况如何?有没有相关的bug?
[4]使用了这两个特性的脚本在使用时还有什么需要注意的地方?
2 defer特性
2.1 关于defer脚本的执行时机
defer特性是HTML4规范中定义的扩展特性,最初只有IE4+和firefox3.5+才支持,之后chrome等浏览器也增加了对它的支持,使用的方式为defer="defer"。defer意为延迟,也就是会延迟脚本的执行。正常情况下,我们引入的脚本会被立即下载和执行,而有了defer特性之后,脚本下载完毕后不会立即执行,而是等到页面解析完毕之后再执行。我们看一下HTML4标准对defer的阐述:
defer:When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.
也就是说,如果设置了defer,那么就告诉用户代理,这个脚本不会产生任何文档内容,从而用户代理可以继续解析和渲染。我们再看一下MDN中对defer的关键描述:
defer:If the async attribute is not present but the defer attribute is present, then the script is executed when the page has finished parsing.
通过标准中的定义,我们可以明确,即:defer的脚本不会阻塞页面的解析,而是等到页面解析结束之后再执行,但是耗时的defer依然可能会阻塞外部资源的下载,那么它会阻塞DOMContentLoaded事件么?事实上,defer的脚本依然是在DOMContentLoaded事件之前执行的,因此它还是会阻塞DOMContentLoaded中的脚本。我们可以通过下图来帮助理解defer脚本的执行时机:
根据标准中的定义,内部脚本不支持defer,而IE9及以下的浏览器则提供了内部脚本的defer支持。
2.2 defer的浏览器支持情况
下面我们来看一下defer特性的浏览器支持情况:
IE9及以下的浏览器存在一个bug,这个bug将在稍后的DEMO中进行详细的说明。
2.3 DEMO:defer特性的功能验证
我们模仿在Olivier Rochard在《the script defer attribute》使用的方式来验证一下defer特性的功能:
首先我们准备了6个外部脚本:
1.js:
test += "我是head外部脚本\n";
2.js
test += "我是body外部脚本\n";
3.js
test += "我是底部外部脚本\n";
defer1.js
test += "我是head外部延迟脚本\n";
defer2.js
test += "我是body外部延迟脚本\n";
defer3.js
test += "我是底部外部延迟脚本\n";
HTML中的代码为: