JavaScript 傾向于阻塞瀏覽器某些處理過(guò)程,如HTTP 請求和界面刷新,這是開(kāi)發(fā)者面臨的最顯著(zhù)的性能問(wèn)題。保持JavaScript文件短小,并限制HTTP請求的數量,只是創(chuàng )建反應迅速的網(wǎng)頁(yè)應用的第一步。一個(gè)應用程序所包含的功能越多,所需要的JavaScript 代碼就越大,保持源碼短小并不總是一種選擇。盡管下載一個(gè)大JavaScript 文件只產(chǎn)生一次HTTP 請求,卻會(huì )鎖定瀏覽器一大段時(shí)間。為避開(kāi)這種情況,你需要向頁(yè)面中逐步添加JavaScript,某種程度上說(shuō)不會(huì )阻塞瀏覽器。非阻塞腳本的秘密在于,等頁(yè)面完成加載之后,再加載JavaScript 源碼。從技術(shù)角度講,這意味著(zhù)在window 的load 事件發(fā)出之后開(kāi)始下載代碼。有三種方法可以實(shí)現這種效果。
延期腳本
HTML 4 為<script>標簽定義了一個(gè)擴展屬性:defer。這個(gè)defer 屬性指明元素中所包含的腳本不打算修改DOM,因此代碼可以稍后執行。defer 屬性只被Internet Explorer 4 和Firefox 3.5 更高版本的瀏覽器所支持,它不是一個(gè)理想的跨瀏覽器解決方案。在其他瀏覽器上,defer 屬性被忽略,<script>標簽按照默認方式被處理(造成阻塞)。如果瀏覽器支持的話(huà),這種方法仍是一種有用的解決方案。示例如下:
1 | <script type="text/javascript" src="file1.js" defer></script> |
一個(gè)帶有defer 屬性的<script>標簽可以放置在文檔的任何位置。對應的JavaScript 文件將在<script>被解析時(shí)啟動(dòng)下載,但代碼不會(huì )被執行,直到DOM 加載完成。(在onload 事件句柄被調用之前)。當一個(gè)defer的JavaScript 文件被下載時(shí),它不會(huì )阻塞瀏覽器的其他處理過(guò)程,所以這些文件可以與頁(yè)面的其他資源一起并行下載。任何帶有defer 屬性的<script>元素在DOM 加載完成之前不會(huì )被執行,不論是內聯(lián)腳本還是外部腳本文件,都是這樣。下面的例子展示了defer 屬性如何影響腳本行為:
03 | <title>Script Defer Example</title> |
13 | window.onload = function(){ |
這些代碼在頁(yè)面處理過(guò)程中彈出三個(gè)對話(huà)框。如果瀏覽器不支持defer 屬性,那么彈出對話(huà)框的順序是"defer","script"和"load"。如果瀏覽器支持defer 屬性,那么彈出對話(huà)框的順序是"script","defer"和"load"。注意,標記為defer 的<script>元素不是跟在第二個(gè)后面運行,而是在onload 事件句柄處理之前被調用。如果你的目標瀏覽器只包括Internet Explorer 和Firefox 3.5,那么defer 腳本確實(shí)有用。如果你需要支持跨領(lǐng)域的多種瀏覽器,那么還有更一致的實(shí)現方式。
動(dòng)態(tài)腳本元素
文檔對象模型(DOM)允許你使用JavaScript 動(dòng)態(tài)創(chuàng )建HTML 的幾乎全部文檔內容。其根本在于,<script>元素與頁(yè)面其他元素沒(méi)有什么不同:引用變量可以通過(guò)DOM 進(jìn)行檢索,可以從文檔中移動(dòng)、刪除,也可以被創(chuàng )建。一個(gè)新的<script>元素可以非常容易地通過(guò)標準DOM 函數創(chuàng )建:
1 | var script = document.createElement ("script"); |
2 | script.type = "text/javascript"; |
3 | script.src = "file1.js"; |
4 | document.getElementsByTagName_r("head")[0].appendChild(script); |
新的<script>元素加載file1.js 源文件。此文件當元素添加到頁(yè)面之后立刻開(kāi)始下載。此技術(shù)的重點(diǎn)在于:無(wú)論在何處啟動(dòng)下載,文件的下載和運行都不會(huì )阻塞其他頁(yè)面處理過(guò)程。你甚至可以將這些代碼放在<head>部分而不會(huì )對其余部分的頁(yè)面代碼造成影響(除了用于下載文件的HTTP 連接)。
當文件使用動(dòng)態(tài)腳本節點(diǎn)下載時(shí),返回的代碼通常立即執行(除了Firefox 和Opera,他們將等待此前的所有動(dòng)態(tài)腳本節點(diǎn)執行完畢)。當腳本是"自運行"類(lèi)型時(shí)這一機制運行正常,但是如果腳本只包含供頁(yè)面其他腳本調用調用的接口,則會(huì )帶來(lái)問(wèn)題。這種情況下,你需要跟蹤腳本下載完成并準備妥善的情況??梢允褂脛?dòng)態(tài)<script>節點(diǎn)發(fā)出事件得到相關(guān)信息。
Firefox, Opera, Chorme 和Safari 3+會(huì )在<script>節點(diǎn)接收完成之后發(fā)出一個(gè)load 事件。你可以監聽(tīng)這一事件,以得到腳本準備好的通知:
1 | var script = document.createElement ("script") |
2 | script.type = "text/javascript"; |
3 | //Firefox, Opera, Chrome, Safari 3+ |
4 | script.onload = function(){ |
5 | alert("Script loaded!"); |
7 | script.src = "file1.js"; |
8 | document.getElementsByTagName("head")[0].appendChild(script); |
Internet Explorer 支持另一種實(shí)現方式,它發(fā)出一個(gè)readystatechange 事件。<script>元素有一個(gè)readyState屬性,它的值隨著(zhù)下載外部文件的過(guò)程而改變。readyState 有五種取值:
- "uninitialized"默認狀態(tài)
- "loading"下載開(kāi)始
- "loaded"下載完成
- "interactive"下載完成但尚不可用
- "complete"所有數據已經(jīng)準備好
微軟文檔上說(shuō),在<script>元素的生命周期中,readyState 的這些取值不一定全部出現,但并沒(méi)有指出哪些取值總會(huì )被用到。實(shí)踐中,我們最感興趣的是"loaded"和"complete"狀態(tài)。Internet Explorer 對這兩個(gè)readyState 值所表示的最終狀態(tài)并不一致,有時(shí)<script>元素會(huì )得到"loader"卻從不出現"complete",但另外一些情況下出現"complete"而用不到"loaded"。最安全的辦法就是在readystatechange 事件中檢查這兩種狀態(tài),并且當其中一種狀態(tài)出現時(shí),刪除readystatechange 事件句柄(保證事件不會(huì )被處理兩次):
01 | var script = document.createElement("script") |
02 | script.type = "text/javascript"; |
04 | script.onreadystatechange = function(){ |
05 | if (script.readyState == "loaded" || script.readyState == "complete"){ |
06 | script.onreadystatechange = null; |
07 | alert("Script loaded."); |
10 | script.src = "file1.js"; |
11 | document.getElementsByTagName("head")[0].appendChild(script); |
大多數情況下,你希望調用一個(gè)函數就可以實(shí)現JavaScript 文件的動(dòng)態(tài)加載。下面的函數封裝了標準實(shí)現和IE 實(shí)現所需的功能:
01 | function loadScript(url, callback){ |
02 | var script = document.createElement ("script") |
03 | script.type = "text/javascript"; |
04 | if (script.readyState){ //IE |
05 | script.onreadystatechange = function(){ |
06 | if (script.readyState == "loaded" || script.readyState == "complete"){ |
07 | script.onreadystatechange = null; |
12 | script.onload = function(){ |
17 | document.getElementsByTagName("head")[0].appendChild(script); |
此函數接收兩個(gè)參數:JavaScript 文件的URL,和一個(gè)當JavaScript 接收完成時(shí)觸發(fā)的回調函數。屬性檢查用于決定監視哪種事件。最后一步,設置src 屬性,并將<script>元素添加至頁(yè)面。此loadScript()函數使用方法如下:
1 | loadScript("file1.js", function(){ |
2 | alert("File is loaded!"); |
你可以在頁(yè)面中動(dòng)態(tài)加載很多JavaScript 文件,但要注意,瀏覽器不保證文件加載的順序。所有主流瀏覽器之中,只有Firefox 和Opera 保證腳本按照你指定的順序執行。其他瀏覽器將按照服務(wù)器返回它們的次序下載并運行不同的代碼文件。你可以將下載操作串聯(lián)在一起以保證他們的次序,如下:
1 | loadScript("file1.js", function(){ |
2 | loadScript("file2.js", function(){ |
3 | loadScript("file3.js", function(){ |
4 | alert("All files are loaded!"); |
此代碼等待file1.js 可用之后才開(kāi)始加載file2.js,等f(wàn)ile2.js 可用之后才開(kāi)始加載file3.js。雖然此方法可行,但如果要下載和執行的文件很多,還是有些麻煩。如果多個(gè)文件的次序十分重要,更好的辦法是將這些文件按照正確的次序連接成一個(gè)文件。獨立文件可以一次性下載所有代碼(由于這是異步進(jìn)行的,使用一個(gè)大文件并沒(méi)有什么損失)。
動(dòng)態(tài)腳本加載是非阻塞JavaScript 下載中最常用的模式,因為它可以跨瀏覽器,而且簡(jiǎn)單易用。
使用XMLHttpRequest(XHR)對象
此技術(shù)首先創(chuàng )建一個(gè)XHR 對象,然后下載JavaScript 文件,接著(zhù)用一個(gè)動(dòng)態(tài)<script>元素將JavaScript 代碼注入頁(yè)面。下面是一個(gè)簡(jiǎn)單的例子:
01 | var xhr = new XMLHttpRequest(); |
02 | xhr.open("get", "file1.js", true); |
03 | xhr.onreadystatechange = function(){ |
04 | if (xhr.readyState == 4){ |
05 | if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ |
06 | var script = document.createElement ("script"); |
07 | script.type = "text/javascript"; |
08 | script.text = xhr.responseText; |
09 | document.body.appendChild(script); |
此代碼向服務(wù)器發(fā)送一個(gè)獲取file1.js 文件的GET 請求。onreadystatechange 事件處理函數檢查readyState是不是4,然后檢查HTTP 狀態(tài)碼是不是有效(2XX 表示有效的回應,304 表示一個(gè)緩存響應)。如果收到了一個(gè)有效的響應,那么就創(chuàng )建一個(gè)新的<script>元素,將它的文本屬性設置為從服務(wù)器接收到的responseText 字符串。這樣做實(shí)際上會(huì )創(chuàng )建一個(gè)帶有內聯(lián)代碼的<script>元素。一旦新<script>元素被添加到文檔,代碼將被執行,并準備使用。這種方法的主要優(yōu)點(diǎn)是,你可以下載不立即執行的JavaScript 代碼。由于代碼返回在<script>標簽之外(換句話(huà)說(shuō)不受<script>標簽約束),它下載后不會(huì )自動(dòng)執行,這使得你可以推遲執行,直到一切都準備好了。另一個(gè)優(yōu)點(diǎn)是,同樣的代碼在所有現代瀏覽器中都不會(huì )引發(fā)異常。此方法最主要的限制是:JavaScript 文件必須與頁(yè)面放置在同一個(gè)域內,不能從CDNs 下載(CDN 指"內容投遞網(wǎng)絡(luò )(Content Delivery Network)",大型網(wǎng)頁(yè)通常不采用XHR 腳本注入技術(shù)。
推薦的向頁(yè)面加載大量JavaScript 的方法分為兩個(gè)步驟:第一步,包含動(dòng)態(tài)加載JavaScript 所需的代碼,然后加載頁(yè)面初始化所需的除JavaScript 之外的部分。這部分代碼盡量小,可能只包含loadScript()函數,它下載和運行非常迅速,不會(huì )對頁(yè)面造成很大干擾。當初始代碼準備好之后,用它來(lái)加載其余的JavaScript。例如:
1 | <script type="text/javascript" src="loader.js"></script> |
2 | <script type="text/javascript"> |
3 | loadScript("the-rest.js", function(){ |
將此代碼放置在body 的關(guān)閉標簽</body>之前。這樣做有幾點(diǎn)好處:首先,像前面討論過(guò)的那樣,這樣做確保JavaScript 運行不會(huì )影響頁(yè)面其他部分顯示。其次,當第二部分JavaScript 文件完成下載,所有應用程序所必須的DOM 已經(jīng)創(chuàng )建好了,并做好被訪(fǎng)問(wèn)的準備,避免使用額外的事件處理(例如window.onload)來(lái)得知頁(yè)面是否已經(jīng)準備好了。另一個(gè)選擇是直接將loadScript()函數嵌入在頁(yè)面中,這可以避免另一次HTTP 請求。例如:
01 | <script type="text/javascript"> |
02 | function loadScript(url, callback){ |
03 | var script = document.createElement ("script") |
04 | script.type = "text/javascript"; |
05 | if (script.readyState){ //IE |
06 | script.onreadystatechange = function(){ |
07 | if (script.readyState == "loaded" || |
08 | script.readyState == "complete"){ |
09 | script.onreadystatechange = null; |
14 | script.onload = function(){ |
19 | document.getElementsByTagName_r("head")[0].appendChild(script); |
21 | loadScript("the-rest.js", function(){ |
如果你決定使用這種方法,建議你使用''YUI Compressor''或者類(lèi)似的工具將初始化腳本縮小到最小字節尺寸。一旦頁(yè)面初始化代碼下載完成,你還可以使用loadScript()函數加載頁(yè)面所需的額外功能函數。