探析瀏覽器執行JavaScript腳本加載與代碼執行順序

本文主要基于向HTML頁面引入JavaScript的幾種方式,分析HTML中JavaScript腳本的執行順序問題
1. 關于JavaScript腳本執行的阻塞性
JavaScript在瀏覽器中被解析和執行時具有阻塞的特性,也就是說,當JavaScript代碼執行時,頁面的解析、渲染以及其他資源的下載都要停下來等待腳本執行完畢① 。這一點是沒有爭議的 , 并且在所有瀏覽器中的行為都是一致的,原因也不難理解:瀏覽器需要一個穩定的DOM結構,而JavaScript可能會修改DOM(改變DOM結構或修改某個DOM節點),如果在JavaScript執行的同時還繼續進行頁面的解析,那么整個解析過程將變得難以控制,解析出錯的可能也變得很大 。
然而這里還有一個問題需要注意,對于外部腳本,還涉及到一個腳本下載的過程,在早期的瀏覽器中 , JavaScript文件的下載不僅會阻塞頁面的解析 , 甚至還會阻塞頁面其他資源的下載(包括其他JavaScript腳本文件、外部CSS文件以及圖片等外部資源) 。從IE8、firefox3.5、safari4和chrome2開始允許JavaScript并行下載,同時JavaScript文件的下載也不會阻塞其他資源的下載(舊版本中,JavaScript文件的下載也會阻塞其他資源的下載) 。
注:不同瀏覽器對于同一個域名下的最大連接數有不同的限制 , HTTP1.1協議規范中的要求是不能高于2個,但是大多數瀏覽器目前實際提供的最大連接數都多于2個,IE6/7都是2個,IE8提升到了6個,firefox和chrome也是6個,當然這個設置也是可以修改的,詳細內容可以參考:http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/
2. 關于腳本的執行順序
瀏覽器是按照從上到下的順序解析頁面,因此正常情況下,JavaScript腳本的執行順序也是從上到下的,即頁面上先出現的代碼或先被引入的代碼總是被先執行,即使是允許并行下載JavaScript文件時也是如此 。注意我們這里標紅了"正常情況下",原因是什么呢?我們知道,在HTML中加入JavaScript代碼有多種方式 , 概括如下(不考慮requirejs或seajs等模塊加載器):
(1)正常引入:即在頁面中通過

探析瀏覽器執行JavaScript腳本加載與代碼執行順序

第5種情況對于我們討論的腳本執行順序沒有什么影響,因此我們這里只討論前四種情況:
2.1 正常引入腳本時
正常引入腳本時,JavaScript代碼會按照從上到下的順序執行,不管腳本是不是并行下載,執行時還是按照引入的順序從上到下執行的,我們以下面的DEMO為例:
首先,通過PHP寫了一個腳本,這個腳本接收兩個參數,文件URL和延遲時間,腳本會在傳入的延遲時間之后,將文件內容發送給瀏覽器,腳本如下:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

另外我們還定義了兩個JavaScript文件,分別為1.js和2.js,在這個例子中,二者的代碼分別如下:
1.js
alert("我是第一個腳本");
2.js
alert("我是第二個腳本");
然后,我們在HTML中引入腳本代碼:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

雖然第一個腳本延遲了3秒才會返回,但是在所有瀏覽器中 , 彈出的順序也都是相同的,即:"我是第一個腳本"->"我是內部腳本"->"我是第二個腳本"

2.2 通過document.write向頁面中寫入腳本時
【探析瀏覽器執行JavaScript腳本加載與代碼執行順序】document.write在文檔流沒有關閉的情況下,會將內容寫入腳本所在位置結束之后緊鄰的位置,瀏覽器執行完當前短的代碼,會接著解析document.write所寫入的內容 。

注:document.write寫入內容的位置還存在一個問題,加入在內部的腳本中寫入了標簽內部不應該出現的內容,比如等內容標簽等,則這段內容的起始位置將是標簽的起始位置 。

通過document.write寫入腳本時存在一些問題,需要分類進行說明:

[1]同一個標簽中通過document.write只寫入外部腳本:

在這種情況下,外部腳本的執行順序總是低于引入腳本的標簽內的代碼,并且按照引入的順序來執行,我們修改HTML中的代碼:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

這段代碼執行完畢之后 , DOM將被修改為:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

探析瀏覽器執行JavaScript腳本加載與代碼執行順序

而代碼執行的結果也符合DOM中腳本的順序:"我是第一個腳本"->"我是內部腳本"->"我是第二個腳本"->"我是第一個腳本"

[2]同一個標簽中通過document.write只寫入內部腳本:

在這種情況下,通過documen.write寫入的內部腳本,執行順序的優先級與寫入腳本標簽內的代碼相同,并且按照寫入的先后順序執行:

我們再修改HTML代碼如下:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

在這種情況下,document.write寫入的腳本被認為與寫入位置處的代碼優先級相同 , 因此在所有瀏覽器中 , 彈出框的順序均為:"我是第一個腳本"->"我是document.write寫入的內部腳本"->"我是內部腳本"->"我是document.write寫入的內部腳本2222"->"我是document.write寫入的內部腳本3333"

[3]同一個標簽中通過document.write同時寫入內部腳本和外部腳本時:

在這種情況下,不同的瀏覽器中存在一些區別:

在IE9及以下的瀏覽器中:只要是通過document.write寫入的內部腳本 , 其優先級總是高于document.write寫入的外部腳本,并且優先級與寫入標簽內的代碼相同 。而通過通過document.write寫入的外部腳本,則總是在寫入標簽的代碼執行完畢后 , 再按照寫入的順序執行;

而在其中瀏覽器中,出現在第一個document.write寫入的外部腳本之前的內部腳本,執行順序的優先級與寫入標簽內的腳本優先級相同,而之后寫入的腳本代碼,不管是內部腳本還是外部腳本,總是要等到寫入標簽內的腳本執行完畢后,再按照寫入的順序執行 。

我們修改以下HTML中的代碼:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

在IE9及以下的瀏覽器中,上面代碼執行后彈出的內容為:"我是第一個腳本"->"我是document.write寫入的內部腳本"->"我是內部腳本"->"我是document.write寫入的內部腳本2222"->"我是document.write寫入的內部腳本3333"->"我是內部腳本2222"->"我是第一個腳本"->"我是第一個腳本"

其他瀏覽器中 , 代碼執行后彈出的內容為:"我是第一個腳本"->"我是document.write寫入的內部腳本"->"我是內部腳本"->"我是內部腳本2222"->"我是第一個腳本"->"我是document.write寫入的內部腳本2222"->"我是第一個腳本"->"我是document.write寫入的內部腳本3333"

如果希望IE及以下的瀏覽器與其他瀏覽器保持一致的行為,那么可選的做法就是把引入內部腳本的代碼拿出來,單獨放在后面一個新的標簽內即可,因為后面標簽中通過document.write所引入的代碼執行順序肯定是在之前的標簽中的代碼的后面的 。
2.3 通過動態腳本技術添加代碼時
通過動態腳本技術添加代碼的主要目的在于創建無阻塞腳本 , 因為通過動態腳本技術添加的代碼不會立刻執行 , 我們可以通過下面的load函數為頁面添加動態腳本:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

但是通過動態腳本技術添加的外部JavaScript腳本不保證按照添加的順序執行,這一點可以通過回調或者使用jQuery的html()方法
2.4 通過Ajax注入腳本
通過Ajax注入腳本同樣也是添加無阻塞腳本的技術之一,我們首先需要創建一個XMLHttpRequest對象,并且實現get方法,然后通過get方法取得腳本內容并注入到文檔中 。
代碼示例:
我們可以用如下代碼封裝XMLHttpRequest對象,并封裝其get方法:
探析瀏覽器執行JavaScript腳本加載與代碼執行順序

然后基于xhr對象,再創建loadXhrScript函數
我們上面的get方法添加了一個參數,即是否異步,那么如果我們采用同步方法 , 通過Ajax注入的腳本肯定是按照添加的順序執行;反之,如果我們采用異步的方案,那么添加的腳本的執行順序肯定是無法確定的 。

相關經驗推薦