JavaScript 傾向於阻塞瀏覽器某些處理過程,如HTTP 要求和介面重新整理,這是開發人員面臨的最顯著的效能問題。保持JavaScript檔案短小,並限制HTTP請求的數量,只是建立反應迅速的網頁應用的第一步。一個應用程式所包含的功能越多,所需要的JavaScript 代碼就越大,保持源碼短小並不總是一種選擇。儘管下載一個大JavaScript 檔案只產生一次HTTP 要求,卻會鎖定瀏覽器一大段時間。為避開這種情況,你需要向頁面中逐步添加JavaScript,某種程度上說不會阻塞瀏覽器。非阻塞指令碼的秘密在於,等頁面完成載入之後,再載入JavaScript 源碼。從技術角度講,這意味著在window 的load 事件發出之後開始下載代碼。有三種方法可以實現這種效果。
延期指令碼
HTML 4 為<script>標籤定義了一個擴充屬性:defer。這個defer 屬性指明元素中所包含的指令碼不打算修改DOM,因此代碼可以稍後執行。defer 屬性只被Internet Explorer 4 和Firefox 3.5 更高版本的瀏覽器所支援,它不是一個理想的跨瀏覽器解決方案。在其他瀏覽器上,defer 屬性被忽略,<script>標籤按照預設被處理(造成阻塞)。如果瀏覽器支援的話,這種方法仍是一種有用的解決方案。樣本如下:
<script type="text/javascript" src="file1.js" defer></script>
一個帶有defer 屬性的<script>標籤可以放置在文檔的任何位置。對應的JavaScript 檔案將在<script>被解析時啟動下載,但代碼不會被執行,直到DOM 載入完成。(在onload 事件控制代碼被調用之前)。當一個defer的JavaScript 檔案被下載時,它不會阻塞瀏覽器的其他處理過程,所以這些檔案可以與頁面的其他資源一起並行下載。任何帶有defer 屬性的<script>元素在DOM 載入完成之前不會被執行,不論是內聯指令碼還是外部指令檔,都是這樣。下面的例子展示了defer 屬性如何影響指令碼行為:
<html> <head> <title>Script Defer Example</title> </head> <body> <script defer> alert("defer"); </script> <script> alert("script"); </script> <script> window.onload = function(){ alert("load"); }; </script> </body> </html>
這些代碼在頁面處理過程中彈出三個對話方塊。如果瀏覽器不支援defer 屬性,那麼彈出對話方塊的順序是"defer","script"和"load"。如果瀏覽器支援defer 屬性,那麼彈出對話方塊的順序是"script","defer"和"load"。注意,標記為defer 的<script>元素不是跟在第二個後面運行,而是在onload 事件控制代碼處理之前被調用。如果你的目標瀏覽器只包括Internet Explorer 和Firefox 3.5,那麼defer 指令碼確實有用。如果你需要支援跨領域的多種瀏覽器,那麼還有更一致的實現方式。
動態指令碼元素
文件物件模型(DOM)允許你使用JavaScript 動態建立HTML 的幾乎全部文檔內容。其根本在於,<script>元素與頁面其他元素沒有什麼不同:引用變數可以通過DOM 進行檢索,可以從文檔中移動、刪除,也可以被建立。一個新的<script>元素可以非常容易地通過標準DOM 函數建立:
var script = document.createElement ("script"); script.type = "text/javascript"; script.src = "file1.js"; document.getElementsByTagName_r("head")[0].appendChild(script);
新的<script>元素載入file1.js 源檔案。此檔案當元素添加到頁面之後立刻開始下載。此技術的重點在於:無論在何處啟動下載,檔案的下載和運行都不會阻塞其他頁面處理過程。你甚至可以將這些代碼放在<head>部分而不會對其餘部分的頁面代碼造成影響(除了用於下載檔案的HTTP 串連)。
當檔案使用動態指令碼節點下載時,返回的代碼通常立即執行(除了Firefox 和Opera,他們將等待此前的所有動態指令碼節點執行完畢)。當指令碼是"自運行"類型時這一機制運行正常,但是如果指令碼只包含供頁面其他指令碼調用調用的介面,則會帶來問題。這種情況下,你需要跟蹤指令碼下載完成並準備妥善的情況。可以使用動態<script>節點發出事件得到相關資訊。
Firefox, Opera, Chorme 和Safari 3+會在<script>節點接收完成之後發出一個load 事件。你可以監聽這一事件,以得到指令碼準備好的通知:
var script = document.createElement ("script") script.type = "text/javascript"; //Firefox, Opera, Chrome, Safari 3+ script.onload = function(){ alert("Script loaded!"); }; script.src = "file1.js"; document.getElementsByTagName("head")[0].appendChild(script);
Internet Explorer 支援另一種實現方式,它發出一個readystatechange 事件。<script>元素有一個readyState屬性,它的值隨著下載外部檔案的過程而改變。readyState 有五種取值:
- "uninitialized"預設狀態
- "loading"下載開始
- "loaded"下載完成
- "interactive"下載完成但尚不可用
- "complete"所有資料已經準備好
微軟文檔上說,在<script>元素的生命週期中,readyState 的這些取值不一定全部出現,但並沒有指出哪些取值總會被用到。實踐中,我們最感興趣的是"loaded"和"complete"狀態。Internet Explorer 對這兩個readyState 值所表示的最終狀態並不一致,有時<script>元素會得到"loader"卻從不出現"complete",但另外一些情況下出現"complete"而用不到"loaded"。最安全的辦法就是在readystatechange 事件中檢查這兩種狀態,並且當其中一種狀態出現時,刪除readystatechange 事件控制代碼(保證事件不會被處理兩次):
var script = document.createElement("script") script.type = "text/javascript"; //Internet Explorer script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; alert("Script loaded."); } }; script.src = "file1.js"; document.getElementsByTagName("head")[0].appendChild(script);
大多數情況下,你希望調用一個函數就可以實現JavaScript 檔案的動態載入。下面的函數封裝了標準實現和IE 實現所需的功能:
function loadScript(url, callback){ var script = document.createElement ("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
此函數接收兩個參數:JavaScript 檔案的URL,和一個當JavaScript 接收完成時觸發的回呼函數。屬性檢查用於決定監視哪種事件。最後一步,設定src 屬性,並將<script>元素添加至頁面。此loadScript()函數使用方法如下:
loadScript("file1.js", function(){ alert("File is loaded!"); });
你可以在頁面中動態載入很多JavaScript 檔案,但要注意,瀏覽器不保證檔案載入的順序。所有主流瀏覽器之中,只有Firefox 和Opera 保證指令碼按照你指定的順序執行。其他瀏覽器將按照伺服器返回它們的次序下載並運行不同的代碼檔案。你可以將下載操作串聯在一起以保證他們的次序,如下:
loadScript("file1.js", function(){ loadScript("file2.js", function(){ loadScript("file3.js", function(){ alert("All files are loaded!"); }); }); });
此代碼等待file1.js 可用之後才開始載入file2.js,等file2.js 可用之後才開始載入file3.js。雖然此方法可行,但如果要下載和執行的檔案很多,還是有些麻煩。如果多個檔案的次序十分重要,更好的辦法是將這些檔案按照正確的次序串連成一個檔案。獨立檔案可以一次性下載所有代碼(由於這是非同步進行的,使用一個大檔案並沒有什麼損失)。
動態指令碼載入是非阻塞JavaScript 下載中最常用的模式,因為它可以跨瀏覽器,而且簡單易用。
使用XMLHttpRequest(XHR)對象
此技術首先建立一個XHR 對象,然後下載JavaScript 檔案,接著用一個動態<script>元素將JavaScript 代碼注入頁面。下面是一個簡單的例子:
var xhr = new XMLHttpRequest(); xhr.open("get", "file1.js", true); xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ var script = document.createElement ("script"); script.type = "text/javascript"; script.text = xhr.responseText; document.body.appendChild(script); } } }; xhr.send(null);
此代碼向伺服器發送一個擷取file1.js 檔案的GET 請求。onreadystatechange 事件處理函數檢查readyState是不是4,然後檢查HTTP 狀態代碼是不是有效(2XX 表示有效回應,304 表示一個緩衝響應)。如果收到了一個有效響應,那麼就建立一個新的<script>元素,將它的文字屬性設定為從伺服器接收到的responseText 字串。這樣做實際上會建立一個帶有內聯代碼的<script>元素。一旦新<script>元素被添加到文檔,代碼將被執行,並準備使用。這種方法的主要優點是,你可以下載不立即執行的JavaScript 代碼。由於代碼返回在<script>標籤之外(換句話說不受<script>標籤約束),它下載後不會自動執行,這使得你可以延遲執行,直到一切都準備好了。另一個優點是,同樣的代碼在所有現代瀏覽器中都不會引發異常。此方法最主要的限制是:JavaScript 檔案必須與頁面放置在同一個域內,不能從CDNs 下載(CDN 指"內容投遞網路(Content Delivery Network)",大型網頁通常不採用XHR 指令碼注入技術。
推薦的向頁面載入大量JavaScript 的方法分為兩個步驟:第一步,包含動態載入JavaScript 所需的代碼,然後載入頁面初始化所需的除JavaScript 之外的部分。這部分代碼盡量小,可能只包含loadScript()函數,它下載和運行非常迅速,不會對頁面造成很大幹擾。當初始代碼準備好之後,用它來載入其餘的JavaScript。例如:
<script type="text/javascript" src="loader.js"></script> <script type="text/javascript"> loadScript("the-rest.js", function(){ Application.init(); }); </script>
將此代碼放置在body 的關閉標籤</body>之前。這樣做有幾點好處:首先,像前面討論過的那樣,這樣做確保JavaScript 運行不會影響頁面其他部分顯示。其次,當第二部分JavaScript 檔案完成下載,所有應用程式所必須的DOM 已經建立好了,並做好被訪問的準備,避免使用額外的事件處理(例如window.onload)來得知頁面是否已經準備好了。另一個選擇是直接將loadScript()函數嵌入在頁面中,這可以避免另一次HTTP 要求。例如:
<script type="text/javascript"> function loadScript(url, callback){ var script = document.createElement ("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName_r("head")[0].appendChild(script); } loadScript("the-rest.js", function(){ Application.init(); }); </script>
如果你決定使用這種方法,建議你使用''YUI Compressor''或者類似的工具將初始化指令碼縮小到最小位元組尺寸。一旦頁面初始化代碼下載完成,你還可以使用loadScript()函數載入頁面所需的額外功能函數。