1. 程式人生 > >瀏覽器環境下JavaScript指令碼載入與執行探析之defer與async特性

瀏覽器環境下JavaScript指令碼載入與執行探析之defer與async特性

defer和async特性相信是很多JavaScript開發者"熟悉而又不熟悉"的兩個特性,從字面上來看,二者的功能很好理解,分別是"延遲指令碼"和"非同步指令碼"的作用。然而,以defer為例,一些細節問題可能開發者卻並不一定熟悉,比如:有了defer特性的指令碼會延遲到什麼時候執行;內部指令碼和外部指令碼是不是都能夠支援defer;defer後的指令碼除了會延遲執行之外,還有哪些特殊的地方等等。本文結合已有的一些文章以及MDN文件中對兩個特性的闡述,對defer和async進行更全面的研究和總結,希望能夠幫助開發者更好地掌握這兩個特性。

1 引言

在中我們提到過,JavaScript程式碼的執行會阻塞頁面的解析渲染以及其他資源的下載,當然由於JavaScript是單執行緒語言,那就意味著在正常情況下,一個頁面中的JavaScript程式碼只能按順序從上到下執行,當然,正如中我們分析的,在某些情況下,比如通過document.write進入指令碼或者通過動態指令碼技術引入指令碼時,JavaScript程式碼的執行順序不一定嚴格按照從上到下的順序,而defer和async也是我們所說的"非正常的情況"。

我們經常會說JavaScript的執行具有阻塞性,而在實際的開發中,我們通常最關心的阻塞,同時也是最影響使用者體驗的阻塞應該是以下幾個方面:

[1]頁面解析和渲染的阻塞

[2]我們寫的頁面初始化指令碼(一般是監聽DOMContentLoaded事件所繫結的指令碼,這部分指令碼是我們希望最先執行的指令碼,因為我們會把和使用者互動最相關的程式碼寫在這裡)

[3]頁面外部資源下載的阻塞(比如圖片)

如果我們有一個耗時的指令碼操作,而這段指令碼又阻塞了上面我們提到的這三個地方,那麼這個網頁的效能或者使用者體驗就非常差了。

defer和async這兩個特性的初衷也是希望能夠解決或者緩解阻塞對於頁面體驗的影響,下面我們就來分析一下這兩個特性,我們主要從以下幾個方面來全方位瞭解這兩個特性:

[1]延遲或非同步的指令碼的執行時機是什麼時候?對於頁面的阻塞情況如何?

[2]內部指令碼和外部指令碼是否都能夠實現延遲或非同步?

[3]瀏覽器對這兩個特性的支援情況如何?有沒有相關的bug?

[4]使用了這兩個特性的指令碼在使用時還有什麼需要注意的地方?

2 defer特性

倫理片 http://www.dotdy.com/

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在使用的方式來驗證一下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"/>

    <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>
<script>
    $(function(){
        test += "我是DOMContentLoaded裡面的指令碼\n";
    })
    window.onload = function(){
        test += "我是window.onload裡面的指令碼\n";
        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內部指令碼
我是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裡面的指令碼

我是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的介面呼叫,那麼後面的指令碼可能會先於該指令碼執行。

我們通過DEMO驗證一下,首先修改1.js的程式碼為(這段程式碼只為模擬,事實上這段程式碼存在極大的效能問題):

document.body.innerHTML = "<div id='div'>我是後來加入的</div>";
document.body.innerHTML += "<div id='div'>我是後來加入的</div>";
document.body.innerHTML += "<div id='div'>我是後來加入的</div>";
alert("我是第1個指令碼");

 2.js

alert("我是第2個指令碼");

 修改HMTL中的程式碼為:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>defer bug in IE=9 test</title>
    <script src="2.js" type="text/javascript"></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=ync" type="text/javascript"></script>
    <script>
        alert("我是同步的指令碼");
    </script>
</head>
<body>
</body>
</html>

 這裡我們借用了中的delayfile指令碼來提供了一個延遲,這個指令碼在支援async的瀏覽器中,彈框的順序一般是:我是同步的指令碼-》我是非同步的指令碼。

3.2 async的瀏覽器支援情況

下面我們來看一下async特性的瀏覽器支援情況:

 

 可以看到,只有IE10+才支援async特性,opera mini不支援async特性,另外,async是不支援內部指令碼的。

3.3 async總結

async指的非同步指令碼,即指令碼非同步載入,載入的過程不會造成阻塞,但是async的指令碼的執行時機是不確定的,而且執行的順序也是不確定的,因此使用async的指令碼應該是不依賴於任何程式碼的指令碼(比如第三方統計程式碼或廣告程式碼),否則就會導致執行出錯。

4 defer和async的優先順序問題

影音先鋒電影http://www.iskdy.com/

這一點比較好理解,標準中規定了:

[1]如果<script>元素同時定義了defer和async特性,則按async來處理(注意:對於不支援async的瀏覽器會直接忽略async特性)

[2]如果<script>元素只定義了defer,則按延遲指令碼的方式處理

[3]如果<script>元素沒有定義defer也沒有定義async,則按正常情況處理,即:指令碼立即載入和執行

由於經驗尚淺,文中難免會出現錯誤,歡迎指正錯誤和交流~~