瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之defer與async特性
defer和async特性相信是很多JavaScript開(kāi)發(fā)者"熟悉而又不熟悉"的兩個(gè)特性,從字面上來(lái)看,二者的功能很好理解,分別是"延遲腳本"和"異步腳本"的作用。然而,以defer為例,一些細(xì)節(jié)問(wèn)題可能開(kāi)發(fā)者卻并不一定熟悉,比如:有了defer特性的腳本會(huì)延遲到什么時(shí)候執(zhí)行;內(nèi)部腳本和外部腳本是不是都能夠支持defer;defer后的腳本除了會(huì)延遲執(zhí)行之外,還有哪些特殊的地方等等。本文結(jié)合已有的一些文章以及MDN文檔中對(duì)兩個(gè)特性的闡述,對(duì)defer和async進(jìn)行更全面的研究和總結(jié),希望能夠幫助開(kāi)發(fā)者更好地掌握這兩個(gè)特性。
1 引言
在《瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之代碼執(zhí)行順序》中我們提到過(guò),JavaScript代碼的執(zhí)行會(huì)阻塞頁(yè)面的解析渲染以及其他資源的下載,當(dāng)然由于JavaScript是單線程語(yǔ)言,那就意味著在正常情況下,一個(gè)頁(yè)面中的JavaScript代碼只能按順序從上到下執(zhí)行,當(dāng)然,正如《瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之代碼執(zhí)行順序》中我們分析的,在某些情況下,比如通過(guò)document.write進(jìn)入腳本或者通過(guò)動(dòng)態(tài)腳本技術(shù)引入腳本時(shí),JavaScript代碼的執(zhí)行順序不一定嚴(yán)格按照從上到下的順序,而defer和async也是我們所說(shuō)的"非正常的情況"。
我們經(jīng)常會(huì)說(shuō)JavaScript的執(zhí)行具有阻塞性,而在實(shí)際的開(kāi)發(fā)中,我們通常最關(guān)心的阻塞,同時(shí)也是最影響用戶體驗(yàn)的阻塞應(yīng)該是以下幾個(gè)方面:
[1]頁(yè)面解析和渲染的阻塞
[2]我們寫的頁(yè)面初始化腳本(一般是監(jiān)聽(tīng)DOMContentLoaded事件所綁定的腳本,這部分腳本是我們希望最先執(zhí)行的腳本,因?yàn)槲覀儠?huì)把和用戶交互最相關(guān)的代碼寫在這里)
[3]頁(yè)面外部資源下載的阻塞(比如圖片)
如果我們有一個(gè)耗時(shí)的腳本操作,而這段腳本又阻塞了上面我們提到的這三個(gè)地方,那么這個(gè)網(wǎng)頁(yè)的性能或者用戶體驗(yàn)就非常差了。
defer和async這兩個(gè)特性的初衷也是希望能夠解決或者緩解阻塞對(duì)于頁(yè)面體驗(yàn)的影響,下面我們就來(lái)分析一下這兩個(gè)特性,我們主要從以下幾個(gè)方面來(lái)全方位了解這兩個(gè)特性:
[1]延遲或異步的腳本的執(zhí)行時(shí)機(jī)是什么時(shí)候?對(duì)于頁(yè)面的阻塞情況如何?
[2]內(nèi)部腳本和外部腳本是否都能夠?qū)崿F(xiàn)延遲或異步?
[3]瀏覽器對(duì)這兩個(gè)特性的支持情況如何?有沒(méi)有相關(guān)的bug?
[4]使用了這兩個(gè)特性的腳本在使用時(shí)還有什么需要注意的地方?
2 defer特性
2.1 關(guān)于defer腳本的執(zhí)行時(shí)機(jī)
defer特性是HTML4規(guī)范中定義的擴(kuò)展特性,最初只有IE4+和firefox3.5+才支持,之后chrome等瀏覽器也增加了對(duì)它的支持,使用的方式為defer="defer"。defer意為延遲,也就是會(huì)延遲腳本的執(zhí)行。正常情況下,我們引入的腳本會(huì)被立即下載和執(zhí)行,而有了defer特性之后,腳本下載完畢后不會(huì)立即執(zhí)行,而是等到頁(yè)面解析完畢之后再執(zhí)行。我們看一下HTML4標(biāo)準(zhǔn)對(duì)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.
也就是說(shuō),如果設(shè)置了defer,那么就告訴用戶代理,這個(gè)腳本不會(huì)產(chǎn)生任何文檔內(nèi)容,從而用戶代理可以繼續(xù)解析和渲染。我們?cè)倏匆幌翸DN中對(duì)defer的關(guān)鍵描述:
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.
通過(guò)標(biāo)準(zhǔn)中的定義,我們可以明確,即:defer的腳本不會(huì)阻塞頁(yè)面的解析,而是等到頁(yè)面解析結(jié)束之后再執(zhí)行,但是耗時(shí)的defer依然可能會(huì)阻塞外部資源的下載,那么它會(huì)阻塞DOMContentLoaded事件么?事實(shí)上,defer的腳本依然是在DOMContentLoaded事件之前執(zhí)行的,因此它還是會(huì)阻塞DOMContentLoaded中的腳本。我們可以通過(guò)下圖來(lái)幫助理解defer腳本的執(zhí)行時(shí)機(jī):
根據(jù)標(biāo)準(zhǔn)中的定義,內(nèi)部腳本不支持defer,而IE9及以下的瀏覽器則提供了內(nèi)部腳本的defer支持。
2.2 defer的瀏覽器支持情況
下面我們來(lái)看一下defer特性的瀏覽器支持情況:
IE9及以下的瀏覽器存在一個(gè)bug,這個(gè)bug將在稍后的DEMO中進(jìn)行詳細(xì)的說(shuō)明。
2.3 DEMO:defer特性的功能驗(yàn)證
我們模仿在Olivier Rochard在《the script defer attribute》使用的方式來(lái)驗(yàn)證一下defer特性的功能:
首先我們準(zhǔn)備了6個(gè)外部腳本:
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èi)部腳本\n"; </script> <script> test += "我是head內(nèi)部腳本\n"; </script> </head> <body> <button id="test">點(diǎn)擊一下</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里面的腳本\n"; }) window.onload = function(){ test += "我是window.onload里面的腳本\n"; var button = document.getElementById("test"); button.onclick = function(){ alert(test); } } </script> </html>
代碼中,為了方便實(shí)現(xiàn)DOMContentLoaded事件,我們引入了jQuery(之后的文章還會(huì)再介紹如何自己實(shí)現(xiàn)兼容的DOMContentLoaded),然后,我們?cè)谀_本的head內(nèi)、body內(nèi)部和body外部分別引入延遲腳本和正常腳本,并且通過(guò)一個(gè)全局的字符串來(lái)記錄每一段代碼的執(zhí)行狀態(tài),我們看一下各個(gè)瀏覽器中的執(zhí)行結(jié)果:
IE7 | IE9 | IE10 | CHROME | firefox |
我是head外部腳本 |
我是head外部腳本 |
我是head外部腳本 |
我是head外部腳本 |
|
從輸出的結(jié)果中我們可以確定,只有IE9及以下瀏覽器支持內(nèi)部延遲腳本,并且defer后的腳本都會(huì)在DOMContentLoaded事件之前觸發(fā),因此也是會(huì)堵塞DOMContentLoaded事件的。
2.4 DEMO:IE<=9的defer特性bug
從2.3節(jié)中的demo可以看出,defer后的腳本還是能夠保持執(zhí)行順序的,也就是按照添加的順序依次執(zhí)行。而在IE<=9中,這個(gè)問(wèn)題存在一個(gè)bug:假如我們向文檔中增加了多個(gè)defer的腳本,而且之前的腳本中有appendChild,innerHTML,insertBefore,replaceChild等修改了DOM的接口調(diào)用,那么后面的腳本可能會(huì)先于該腳本執(zhí)行??梢詤⒖糶ithub的issue:https://github.com/h5bp/lazyweb-requests/issues/42
我們通過(guò)DEMO驗(yàn)證一下,首先修改1.js的代碼為(這段代碼只為模擬,事實(shí)上這段代碼存在極大的性能問(wèn)題):
document.body.innerHTML = "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
document.body.innerHTML += "<div id='div'>我是后來(lái)加入的</div>";
alert("我是第1個(gè)腳本");
2.js
alert("我是第2個(gè)腳本");
修改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個(gè)腳本-》我是第2個(gè)腳本,然而在IE<=9中,執(zhí)行結(jié)果卻為:我是第2個(gè)腳本-》我是第1個(gè)腳本,驗(yàn)證了這個(gè)bug。
2.5 defer總結(jié)
在總結(jié)之前,首先要說(shuō)一個(gè)注意點(diǎn):正如標(biāo)準(zhǔn)中提到的,defer的腳本中不應(yīng)該出現(xiàn)document.write的操作,瀏覽器會(huì)直接忽略這些操作。
總的來(lái)看,defer的作用一定程度上與將腳本放置在頁(yè)面底部有一定的相似,但由于IE<=9中的bug,如果頁(yè)面中出現(xiàn)多個(gè)defer時(shí),腳本的執(zhí)行順序可能會(huì)被打亂從而導(dǎo)致代碼依賴可能會(huì)出錯(cuò),因此實(shí)際項(xiàng)目中很少會(huì)使用defer特性,而將腳本代碼放置在頁(yè)面底部可以替代defer所提供的功能。
3 async特性
3.1 關(guān)于async腳本的執(zhí)行時(shí)機(jī)
async特性是HTML5中引入的特性,使用方式為:async="async",我們首先看一下標(biāo)準(zhǔn)中對(duì)于async特性的相關(guān)描述:
async:If the async attribute is present, then the script will be executed asynchronously, as soon as it is available.
需要指出,這里的異步,指的其實(shí)是異步加載而不是異步執(zhí)行,也就是說(shuō),瀏覽器遇到一個(gè)async的script標(biāo)簽時(shí),會(huì)異步的去加載(個(gè)人認(rèn)為這個(gè)過(guò)程主要是下載的過(guò)程),一旦加載完畢就會(huì)執(zhí)行代碼,而執(zhí)行的過(guò)程肯定還是同步的,也就是阻塞的。我們可以通過(guò)下圖來(lái)綜合理解defer和async:
這樣來(lái)看的話,async腳本的執(zhí)行時(shí)機(jī)是無(wú)法確定的,因?yàn)槟_本何時(shí)加載完畢也是不確定的。我們通過(guò)下面的demo來(lái)感受一下:
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>
這里我們借用了《瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之代碼執(zhí)行順序》中的delayfile腳本來(lái)提供了一個(gè)延遲,這個(gè)腳本在支持async的瀏覽器中,彈框的順序一般是:我是同步的腳本-》我是異步的腳本。
3.2 async的瀏覽器支持情況
下面我們來(lái)看一下async特性的瀏覽器支持情況:
可以看到,只有IE10+才支持async特性,opera mini不支持async特性,另外,async是不支持內(nèi)部腳本的。
3.3 async總結(jié)
async指的異步腳本,即腳本異步加載,加載的過(guò)程不會(huì)造成阻塞,但是async的腳本的執(zhí)行時(shí)機(jī)是不確定的,而且執(zhí)行的順序也是不確定的,因此使用async的腳本應(yīng)該是不依賴于任何代碼的腳本(比如第三方統(tǒng)計(jì)代碼或廣告代碼),否則就會(huì)導(dǎo)致執(zhí)行出錯(cuò)。
4 defer和async的優(yōu)先級(jí)問(wèn)題
這一點(diǎn)比較好理解,標(biāo)準(zhǔn)中規(guī)定了:
[1]如果<script>元素同時(shí)定義了defer和async特性,則按async來(lái)處理(注意:對(duì)于不支持async的瀏覽器會(huì)直接忽略async特性)
[2]如果<script>元素只定義了defer,則按延遲腳本的方式處理
[3]如果<script>元素沒(méi)有定義defer也沒(méi)有定義async,則按正常情況處理,即:腳本立即加載和執(zhí)行
- JS中script標(biāo)簽defer和async屬性的區(qū)別詳解
- 關(guān)于Javascript中defer和async的區(qū)別總結(jié)
- 淺析script標(biāo)簽中的defer與async屬性
- JavaScript無(wú)阻塞加載和defer、async詳解
- JS script腳本中async和defer區(qū)別詳解
- JavaScript延遲加載之a(chǎn)sync與defer的應(yīng)用
- script標(biāo)簽中的defer和async使用技巧說(shuō)明
- script標(biāo)簽中的async和defer詳細(xì)說(shuō)明與使用場(chǎng)景
相關(guān)文章
如何基于webRTC實(shí)現(xiàn)人臉識(shí)別功能
WebRTC技術(shù)包含了音視頻編解碼技術(shù)、傳輸技術(shù)、流媒體服務(wù)器技術(shù)等,涵蓋了音視頻處理和傳輸?shù)姆椒矫婷?下面這篇文章主要給大家介紹了關(guān)于如何基于webRTC實(shí)現(xiàn)人臉識(shí)別的相關(guān)資料,需要的朋友可以參考下2022-12-12淺析JavaScript中兩種類型的全局對(duì)象/函數(shù)
這篇文章主要介紹了JavaScript中兩種類型的全局對(duì)象/函數(shù)。需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12多次注冊(cè)事件會(huì)導(dǎo)致一個(gè)事件被觸發(fā)多次的解決方法
一個(gè)JavaScript邏輯,會(huì)自動(dòng)綁定函數(shù)到按鈕的click事件,但是這段代碼會(huì)反復(fù)注冊(cè)事件,具體的解決方法如下,感興趣的朋友可以參考下2013-08-08使用js對(duì)select動(dòng)態(tài)添加和刪除OPTION示例代碼
動(dòng)態(tài)刪除select中的所有options、某一項(xiàng)option以及動(dòng)態(tài)添加select中的項(xiàng)option,在IE和FireFox都能測(cè)試成功,感興趣的朋友可以參考下,希望對(duì)大家有所幫助2013-08-08javascript使用正則表達(dá)式檢測(cè)IP地址
這篇文章主要介紹了javascript使用正則表達(dá)式檢測(cè)IP地址的方法,需要的朋友可以參考下2014-12-12js實(shí)現(xiàn)轉(zhuǎn)盤抽獎(jiǎng)功能
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)轉(zhuǎn)盤抽獎(jiǎng)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03