探析瀏覽器執(zhí)行JavaScript腳本加載與代碼執(zhí)行順序
本文主要基于向HTML頁(yè)面引入JavaScript的幾種方式,分析HTML中JavaScript腳本的執(zhí)行順序問(wèn)題
1. 關(guān)于JavaScript腳本執(zhí)行的阻塞性
JavaScript在瀏覽器中被解析和執(zhí)行時(shí)具有阻塞的特性,也就是說(shuō),當(dāng)JavaScript代碼執(zhí)行時(shí),頁(yè)面的解析、渲染以及其他資源的下載都要停下來(lái)等待腳本執(zhí)行完畢①。這一點(diǎn)是沒(méi)有爭(zhēng)議的,并且在所有瀏覽器中的行為都是一致的,原因也不難理解:瀏覽器需要一個(gè)穩(wěn)定的DOM結(jié)構(gòu),而JavaScript可能會(huì)修改DOM(改變DOM結(jié)構(gòu)或修改某個(gè)DOM節(jié)點(diǎn)),如果在JavaScript執(zhí)行的同時(shí)還繼續(xù)進(jìn)行頁(yè)面的解析,那么整個(gè)解析過(guò)程將變得難以控制,解析出錯(cuò)的可能也變得很大。
然而這里還有一個(gè)問(wèn)題需要注意,對(duì)于外部腳本,還涉及到一個(gè)腳本下載的過(guò)程,在早期的瀏覽器中,JavaScript文件的下載不僅會(huì)阻塞頁(yè)面的解析,甚至還會(huì)阻塞頁(yè)面其他資源的下載(包括其他JavaScript腳本文件、外部CSS文件以及圖片等外部資源)。從IE8、firefox3.5、safari4和chrome2開(kāi)始允許JavaScript并行下載,同時(shí)JavaScript文件的下載也不會(huì)阻塞其他資源的下載(舊版本中,JavaScript文件的下載也會(huì)阻塞其他資源的下載)。
注:不同瀏覽器對(duì)于同一個(gè)域名下的最大連接數(shù)有不同的限制,HTTP1.1協(xié)議規(guī)范中的要求是不能高于2個(gè),但是大多數(shù)瀏覽器目前實(shí)際提供的最大連接數(shù)都多于2個(gè),IE6/7都是2個(gè),IE8提升到了6個(gè),firefox和chrome也是6個(gè),當(dāng)然這個(gè)設(shè)置也是可以修改的,詳細(xì)內(nèi)容可以參考:http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/
2. 關(guān)于腳本的執(zhí)行順序
瀏覽器是按照從上到下的順序解析頁(yè)面,因此正常情況下,JavaScript腳本的執(zhí)行順序也是從上到下的,即頁(yè)面上先出現(xiàn)的代碼或先被引入的代碼總是被先執(zhí)行,即使是允許并行下載JavaScript文件時(shí)也是如此。注意我們這里標(biāo)紅了"正常情況下",原因是什么呢?我們知道,在HTML中加入JavaScript代碼有多種方式,概括如下(不考慮requirejs或seajs等模塊加載器):
(1)正常引入:即在頁(yè)面中通過(guò)<script>標(biāo)簽引入腳本代碼或者引入外部腳本
(2)通過(guò)document.write方法向頁(yè)面寫(xiě)入<script>標(biāo)簽或代碼
(3)通過(guò)動(dòng)態(tài)腳本技術(shù),即利用DOM接口創(chuàng)建<script>元素,并設(shè)置元素的src,然后再將元素添加進(jìn)DOM中。
(4)通過(guò)Ajax獲取腳本內(nèi)容,然后再創(chuàng)建<script>元素,并設(shè)置元素的text,再將元素添加進(jìn)DOM中。
(5)直接把JavaScript代碼寫(xiě)在元素的事件處理程序中或直接作為URL的主體,示例如下:
<!--直接寫(xiě)在元素的事件處理程序中--> <input type="button" value="點(diǎn)擊測(cè)試一下" onclick="alert('點(diǎn)擊了按鈕')"/> <!--作為URL的主體--> <a href="javascript:alert('dd')">JS腳本作為URL的主體</a>
第5種情況對(duì)于我們討論的腳本執(zhí)行順序沒(méi)有什么影響,因此我們這里只討論前四種情況:
2.1 正常引入腳本時(shí)
正常引入腳本時(shí),JavaScript代碼會(huì)按照從上到下的順序執(zhí)行,不管腳本是不是并行下載,執(zhí)行時(shí)還是按照引入的順序從上到下執(zhí)行的,我們以下面的DEMO為例:
首先,通過(guò)PHP寫(xiě)了一個(gè)腳本,這個(gè)腳本接收兩個(gè)參數(shù),文件URL和延遲時(shí)間,腳本會(huì)在傳入的延遲時(shí)間之后,將文件內(nèi)容發(fā)送給瀏覽器,腳本如下:
<?php $url = $_GET['url']; $delay = $_GET['delay']; if(isset($delay)){ sleep($delay); } echo file_get_contents($url); ?>
另外我們還定義了兩個(gè)JavaScript文件,分別為1.js和2.js,在這個(gè)例子中,二者的代碼分別如下:
1.js
alert("我是第一個(gè)腳本");
2.js
alert("我是第二個(gè)腳本");
然后,我們?cè)贖TML中引入腳本代碼:
<script src='/delayfile.php?url=http://localhost/js/load/1.js&delay=3' type='text/javascript'></script> <script type="text/javascript"> alert("我是內(nèi)部腳本"); </script> <script src='/delayfile.php?url=http://localhost/js/load/2.js&delay=1' type='text/javascript'></script>
雖然第一個(gè)腳本延遲了3秒才會(huì)返回,但是在所有瀏覽器中,彈出的順序也都是相同的,即:"我是第一個(gè)腳本"->"我是內(nèi)部腳本"->"我是第二個(gè)腳本"
2.2 通過(guò)document.write向頁(yè)面中寫(xiě)入腳本時(shí)
document.write在文檔流沒(méi)有關(guān)閉的情況下,會(huì)將內(nèi)容寫(xiě)入腳本所在位置結(jié)束之后緊鄰的位置,瀏覽器執(zhí)行完當(dāng)前短的代碼,會(huì)接著解析document.write所寫(xiě)入的內(nèi)容。
注:document.write寫(xiě)入內(nèi)容的位置還存在一個(gè)問(wèn)題,加入在<head>內(nèi)部的腳本中寫(xiě)入了<head>標(biāo)簽內(nèi)部不應(yīng)該出現(xiàn)的內(nèi)容,比如<div>等內(nèi)容標(biāo)簽等,則這段內(nèi)容的起始位置將是<body>標(biāo)簽的起始位置。
通過(guò)document.write寫(xiě)入腳本時(shí)存在一些問(wèn)題,需要分類進(jìn)行說(shuō)明:
[1]同一個(gè)<script>標(biāo)簽中通過(guò)document.write只寫(xiě)入外部腳本:
在這種情況下,外部腳本的執(zhí)行順序總是低于引入腳本的標(biāo)簽內(nèi)的代碼,并且按照引入的順序來(lái)執(zhí)行,我們修改HTML中的代碼:
<script src='/delayfile.php?url=http://localhost/js/load/1.js&delay=2' type='text/javascript'></script> <script type="text/javascript"> document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/2.js"><\/script>'); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); alert("我是內(nèi)部腳本"); </script>
這段代碼執(zhí)行完畢之后,DOM將被修改為:
而代碼執(zhí)行的結(jié)果也符合DOM中腳本的順序:"我是第一個(gè)腳本"->"我是內(nèi)部腳本"->"我是第二個(gè)腳本"->"我是第一個(gè)腳本"
[2]同一個(gè)<script>標(biāo)簽中通過(guò)document.write只寫(xiě)入內(nèi)部腳本:
在這種情況下,通過(guò)documen.write寫(xiě)入的內(nèi)部腳本,執(zhí)行順序的優(yōu)先級(jí)與寫(xiě)入腳本標(biāo)簽內(nèi)的代碼相同,并且按照寫(xiě)入的先后順序執(zhí)行:
我們?cè)傩薷腍TML代碼如下:
<script src='/delayfile.php?url=http://localhost/js/load/1.js' type='text/javascript'></script> <script type="text/javascript"> document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本")<\/script>'); alert("我是內(nèi)部腳本"); document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本2222")<\/script>'); document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本3333")<\/script>'); </script>
在這種情況下,document.write寫(xiě)入的腳本被認(rèn)為與寫(xiě)入位置處的代碼優(yōu)先級(jí)相同,因此在所有瀏覽器中,彈出框的順序均為:"我是第一個(gè)腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本"->"我是內(nèi)部腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本2222"->"我是document.write寫(xiě)入的內(nèi)部腳本3333"
[3]同一個(gè)<script>標(biāo)簽中通過(guò)document.write同時(shí)寫(xiě)入內(nèi)部腳本和外部腳本時(shí):
在這種情況下,不同的瀏覽器中存在一些區(qū)別:
在IE9及以下的瀏覽器中:只要是通過(guò)document.write寫(xiě)入的內(nèi)部腳本,其優(yōu)先級(jí)總是高于document.write寫(xiě)入的外部腳本,并且優(yōu)先級(jí)與寫(xiě)入標(biāo)簽內(nèi)的代碼相同。而通過(guò)通過(guò)document.write寫(xiě)入的外部腳本,則總是在寫(xiě)入標(biāo)簽的代碼執(zhí)行完畢后,再按照寫(xiě)入的順序執(zhí)行;
而在其中瀏覽器中, 出現(xiàn)在第一個(gè)document.write寫(xiě)入的外部腳本之前的內(nèi)部腳本,執(zhí)行順序的優(yōu)先級(jí)與寫(xiě)入標(biāo)簽內(nèi)的腳本優(yōu)先級(jí)相同,而之后寫(xiě)入的腳本代碼,不管是內(nèi)部腳本還是外部腳本,總是要等到寫(xiě)入標(biāo)簽內(nèi)的腳本執(zhí)行完畢后,再按照寫(xiě)入的順序執(zhí)行。
我們修改以下HTML中的代碼:
<script src='/delayfile.php?url=http://localhost/js/load/1.js' type='text/javascript'></script><script type="text/javascript"> document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本")<\/script>'); alert("我是內(nèi)部腳本"); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本2222")<\/script>'); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); document.write('<script type="text/javascript">alert("我是docment.write寫(xiě)入的內(nèi)部腳本3333")<\/script>'); alert("我是內(nèi)部腳本2222");</script>
在IE9及以下的瀏覽器中,上面代碼執(zhí)行后彈出的內(nèi)容為:"我是第一個(gè)腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本"->"我是內(nèi)部腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本2222"->"我是document.write寫(xiě)入的內(nèi)部腳本3333"->"我是內(nèi)部腳本2222"->"我是第一個(gè)腳本"->"我是第一個(gè)腳本"
其他瀏覽器中,代碼執(zhí)行后彈出的內(nèi)容為:"我是第一個(gè)腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本"->"我是內(nèi)部腳本"->"我是內(nèi)部腳本2222"->"我是第一個(gè)腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本2222"->"我是第一個(gè)腳本"->"我是document.write寫(xiě)入的內(nèi)部腳本3333"
如果希望IE及以下的瀏覽器與其他瀏覽器保持一致的行為,那么可選的做法就是把引入內(nèi)部腳本的代碼拿出來(lái),單獨(dú)放在后面一個(gè)新的<script>標(biāo)簽內(nèi)即可,因?yàn)楹竺?lt;script>標(biāo)簽中通過(guò)document.write所引入的代碼執(zhí)行順序肯定是在之前的標(biāo)簽中的代碼的后面的。
2.3 通過(guò)動(dòng)態(tài)腳本技術(shù)添加代碼時(shí)
通過(guò)動(dòng)態(tài)腳本技術(shù)添加代碼的主要目的在于創(chuàng)建無(wú)阻塞腳本,因?yàn)橥ㄟ^(guò)動(dòng)態(tài)腳本技術(shù)添加的代碼不會(huì)立刻執(zhí)行,我們可以通過(guò)下面的load函數(shù)為頁(yè)面添加動(dòng)態(tài)腳本:
function loadScript(url,callback){ var script = document.createElement("script"); script.type = "text/javascript"; //綁定加載完畢的事件 if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState === "loaded" || script.readyState === "complete"){ callback&&callback(); } } }else{ script.onload = function(){ callback&&callback(); } } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
但是通過(guò)動(dòng)態(tài)腳本技術(shù)添加的外部JavaScript腳本不保證按照添加的順序執(zhí)行,這一點(diǎn)可以通過(guò)回調(diào)或者使用jQuery的html()方法,詳細(xì)可參考:http://chabaoo.cn/article/26446.htm
2.4 通過(guò)Ajax注入腳本
通過(guò)Ajax注入腳本同樣也是添加無(wú)阻塞腳本的技術(shù)之一,我們首先需要?jiǎng)?chuàng)建一個(gè)XMLHttpRequest對(duì)象,并且實(shí)現(xiàn)get方法,然后通過(guò)get方法取得腳本內(nèi)容并注入到文檔中。
代碼示例:
我們可以用如下代碼封裝XMLHttpRequest對(duì)象,并封裝其get方法:
var xhr = (function(){ function createXhr(){ var xhr ; if(window.XMLHttpRequest){ xhr = new XMLHttpRequest(); }else if(window.ActiveXObject){ var xhrVersions = ['MSXML2.XMLHttp','MSXML2.XMLHttp.3.0','MSXML2.XMLHttp.6.0'], i, len; for(i = 0, len = xhrVersions.length; i < len ; i++){ try{ xhr = new ActiveXObject(xhrVersions[i]); }catch(e){ } } }else{ throw new Error("無(wú)法創(chuàng)建xhr對(duì)象"); } return xhr; } function get(url,async,callback){ var xhr = createXhr(); xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ callback&&callback(xhr.responseText); }else{ alert("請(qǐng)求失敗,錯(cuò)誤碼為" + xhr.status); } } } xhr.open("get",url,async); xhr.send(null); } return { get:get } }())
然后基于xhr對(duì)象,再創(chuàng)建loadXhrScript函數(shù):
function loadXhrScript(url,async, callback){ if(async == undefined){ async = true; } xhr.get(url,async,function(text){ var script = document.createElement("script"); script.type = "text/javascript"; script.text = text; document.body.appendChild(script); });}
我們上面的get方法添加了一個(gè)參數(shù),即是否異步,那么如果我們采用同步方法,通過(guò)Ajax注入的腳本肯定是按照添加的順序執(zhí)行;反之,如果我們采用異步的方案,那么添加的腳本的執(zhí)行順序肯定是無(wú)法確定的。
- Selenium執(zhí)行JavaScript腳本的方法示例
- Selenium執(zhí)行Javascript腳本參數(shù)及返回值過(guò)程詳解
- js腳本中執(zhí)行java后臺(tái)代碼方法解析
- Java執(zhí)行JS腳本工具
- JS腳本加載后執(zhí)行相應(yīng)回調(diào)函數(shù)的操作方法
- Angularjs通過(guò)指令監(jiān)聽(tīng)ng-repeat渲染完成后執(zhí)行腳本的方法
- JS動(dòng)態(tài)加載腳本并執(zhí)行回調(diào)操作
- node.js 動(dòng)態(tài)執(zhí)行腳本
- 瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之動(dòng)態(tài)腳本與Ajax腳本注入
- 瀏覽器環(huán)境下JavaScript腳本加載與執(zhí)行探析之defer與async特性
- 詳解javascript腳本何時(shí)會(huì)被執(zhí)行
相關(guān)文章
如何在父窗口中得知window.open()出的子窗口關(guān)閉事件
在父窗口中得知window.open()出的子窗口關(guān)閉事件的方法有很多,在本文將為大家詳細(xì)介紹下,感興趣的朋友可以參考下2013-10-10js 公式編輯器 - 自定義匹配規(guī)則 - 帶提示下拉框 - 動(dòng)態(tài)獲取光標(biāo)像素坐標(biāo)
這篇文章主要介紹了js公式編輯器 - 自定義匹配規(guī)則 - 帶提示下拉框 - 動(dòng)態(tài)獲取光標(biāo)像素坐標(biāo)的方法,,需要的朋友可以參考下2018-01-01JS回調(diào)函數(shù)簡(jiǎn)單易懂的入門(mén)實(shí)例分析
這篇文章主要介紹了JS回調(diào)函數(shù),結(jié)合簡(jiǎn)單實(shí)例形式分析了javascript回調(diào)函數(shù)的概念、原理、相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2019-09-09webpack cjs運(yùn)行時(shí)分析示例詳解
這篇文章主要介紹了webpack cjs運(yùn)行時(shí)分析,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12js獲取json中key所對(duì)應(yīng)的value值的簡(jiǎn)單方法
下面小編就為大家?guī)?lái)一篇js獲取json中key所對(duì)應(yīng)的value值的簡(jiǎn)單方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03JavaScript轉(zhuǎn)換與解析JSON方法實(shí)例詳解
這篇文章主要介紹了JavaScript轉(zhuǎn)換與解析JSON方法,實(shí)例分析了JavaScript解析json的技巧,并附帶分析了jQuery解析與轉(zhuǎn)換json的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11JavaScript學(xué)習(xí)總結(jié)之正則的元字符和一些簡(jiǎn)單的應(yīng)用
這篇文章主要介紹了JavaScript學(xué)習(xí)總結(jié)之正則的元字符和一些簡(jiǎn)單的應(yīng)用,需要的朋友可以參考下2017-06-06js實(shí)現(xiàn)圖片輪播效果學(xué)習(xí)筆記
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)圖片輪播效果的學(xué)習(xí)筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07