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, continue agent cancan continue a. 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中的程式碼為:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>defer attribute test</title> <script src="http://lib.sinaapp.com/js/jquery/1.9.1/jquery-1.9.1.min.js"></script> <script type="text/javascript">var test = "";</script> <script src="defer1.js" type="text/javascript" defer="defer"></script> <script src="1.js" type="text/javascript"></script> <script defer="defer"> test += "我是head延迟内部脚本\n"; </script> <script> test += "我是head内部脚本\n"; </script> </head> <body> <button id="test">点击一下</button> <script src="defer2.js" type="text/javascript" defer="defer"></script> <script src="2.js" type="text/javascript"></script> </body> <script src="defer3.js" type="text/javascript" defer="defer"></script> <script src="3.js" type="text/javascript"></script> <script> $(function(){ test += "我是DOMContentLoaded里面的脚本 "; }) window.onload = function(){ test += "我是window.onload里面的脚本 "; var button = document.getElementById("test"); button.onclick = function(){ alert(test); } } </script> </html>
程式碼中,為了方便實作DOMContentLoaded事件,我們引入了jQuery(之後的文章還會再介紹如何自行實作相容的DOMContentLoaded),然後,我們在腳本的head內、body內部和body外部分別引入延遲腳本和正常腳本,並且透過一個全域的字串來記錄每一段程式碼的執行狀態,我們看一下各個瀏覽器中的執行結果:
IE7 | IE9 | IE10 | CHROME | firefox | ||||||||||
|
<🎜>我是head外部腳本<🎜> 我是head內部腳本<🎜> 我是body外部腳本<🎜> 我是底部外部腳本<🎜> 我是head外部延遲腳本<🎜> 我是head延遲內部腳本<🎜> 我是body外部延遲腳本<🎜> 我是底部外部延遲腳本<🎜> 我是DOMContentLoaded裡面的腳本<🎜> 我是window.onload裡面的腳本<🎜> | <🎜>我是head外部腳本<🎜> 我是head延遲內部腳本<🎜> 我是head內部腳本<🎜> 我是body外部腳本<🎜> 我是底部外部腳本<🎜> 我是head外部延遲腳本<🎜> 我是body外部延遲腳本<🎜> 我是底部外部延遲腳本<🎜> 我是DOMContentLoaded裡面的腳本<🎜> 我是window.onload裡面的腳本<🎜> | <🎜>我是head外部腳本<🎜> 我是head延遲內部腳本<🎜> 我是head內部腳本<🎜> 我是body外部腳本<🎜> 我是底部外部腳本<🎜> 我是head外部延遲腳本<🎜> 我是body外部延遲腳本<🎜> 我是底部外部延遲腳本<🎜> 我是DOMContentLoaded裡面的腳本<🎜> 我是window.onload裡面的腳本<🎜> | <🎜> |
从输出的结果中我们可以确定,只有IE9及以下浏览器支持内部延迟脚本,并且defer后的脚本都会在DOMContentLoaded事件之前触发,因此也是会堵塞DOMContentLoaded事件的。
2.4 DEMO:IE<=9的defer特性bug
从2.3节中的demo可以看出,defer后的脚本还是能够保持执行顺序的,也就是按照添加的顺序依次执行。而在IE<=9中,这个问题存在一个bug:假如我们向文档中增加了多个defer的脚本,而且之前的脚本中有appendChild,innerHTML,insertBefore,replaceChild等修改了DOM的接口调用,那么后面的脚本可能会先于该脚本执行。可以参考github的issue:https://github.com/h5bp/lazyweb-requests/issues/42
我们通过DEMO验证一下,首先修改1.js的代码为(这段代码只为模拟,事实上这段代码存在极大的性能问题):
document.body.innerHTML = "
2.js
alert("我是第2个脚本");
修改HMTL中的代码为:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>defer bug in IE=9 test</title> <script src="1.js" type="text/javascript" defer="defer"></script> <script src="2.js" type="text/javascript" defer="defer"></script> </head> <body> </body> </html>
正常情况下,浏览器中弹出框的顺序肯定是:我是第1个脚本-》我是第2个脚本,然而在IE<=9中,执行结果却为:我是第2个脚本-》我是第1个脚本,验证了这个bug。
2.5 defer总结
在总结之前,首先要说一个注意点:正如标准中提到的,defer的脚本中不应该出现document.write的操作,浏览器会直接忽略这些操作。
总的来看,defer的作用一定程度上与将脚本放置在页面底部有一定的相似,但由于IE<=9中的bug,如果页面中出现多个defer时,脚本的执行顺序可能会被打乱从而导致代码依赖可能会出错,因此实际项目中很少会使用defer特性,而将脚本代码放置在页面底部可以替代defer所提供的功能。
3 async特性
3.1 关于async脚本的执行时机
async特性是HTML5中引入的特性,使用方式为:async="async",我们首先看一下标准中对于async特性的相关描述:
async:If the async attribute is present, then the script will be executed asynchronously, as soon as it is available.
需要指出,这里的异步,指的其实是异步加载而不是异步执行,也就是说,浏览器遇到一个async的script标签时,会异步的去加载(个人认为这个过程主要是下载的过程),一旦加载完毕就会执行代码,而执行的过程肯定还是同步的,也就是阻塞的。我们可以通过下图来综合理解defer和async:
这样来看的话,async脚本的执行时机是无法确定的,因为脚本何时加载完毕也是不确定的。我们通过下面的demo来感受一下:
async1.js
alert("我是异步的脚本");
HTML代码:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>async attribute test</title> <script src="/delayfile.php?url=http://localhost/js/load/async1.js&delay=2" async="async" type="text/javascript"></script> <script> alert("我是同步的脚本"); </script> </head> <body> </body> </html>
ここでは、「ブラウザ環境での JavaScript スクリプトの読み込みと実行の分析: コード実行シーケンス」の遅延ファイル スクリプトを借用して、非同期をサポートするブラウザでは、このスクリプトのポップアップ ボックスの順序は通常次のとおりです。 : 私は同期スクリプトです -> 私は非同期スクリプトです。
3.2 ブラウザによる非同期のサポート
ブラウザによる非同期機能のサポートを見てみましょう:
ご覧のとおり、IE10+ のみが非同期機能をサポートしており、Opera mini は非同期機能をサポートしておらず、非同期は内部スクリプトをサポートしていません。
3.3 非同期の概要
async は非同期スクリプトを指します。つまり、スクリプトの読み込みプロセスはブロックを引き起こしませんが、async スクリプトの実行タイミングは不確実であり、実行順序も不確実であるため、async を使用するスクリプトは非同期である必要があります。コード (サードパーティの統計コードや広告コードなど) に依存しないスクリプト。そうでない場合は、実行エラーが発生します。
4 遅延と非同期の間の優先度の問題
これは、標準で次のように規定されており、理解しやすいです。
[1]