讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議174:使腳本延遲執行 >

建議174:使腳本延遲執行

JavaScript會阻塞瀏覽器的某些處理過程,如HTTP請求和界面刷新,這是開發者面臨的最重要的性能問題。保持JavaScript文件短小,並限制HTTP請求的數量,只是創建反應迅速的網頁應用的第一步。一個應用程序所包含的功能越多,所需要的JavaScript代碼就越大,保持源代碼短小並不總是最佳選擇。儘管下載一個大JavaScript文件只產生一次HTTP請求,還是會鎖定瀏覽器一大段時間。為了避開這種情況,需要逐步添加JavaScript。

(1)非阻塞腳本技巧

等頁面完成加載之後,再加載JavaScript源代碼。這意味著在window的load事件發出之後開始下載JavaScript源代碼。

HTML 4為<script>標籤定義了一個擴展屬性:defer。這個defer屬性指明元素中所包含的腳本暫時不修改DOM,因此代碼可以稍後執行。defer屬性只被IE 4和Firefox 3.5及其更高版本的瀏覽器所支持,它不是一個理想的跨瀏覽器解決方案。在其他瀏覽器上,defer屬性被忽略,<script>標籤按照默認方式處理,這樣又會造成阻塞。如果瀏覽器支持defer屬性,那麼這種方法仍是一種有用的解決方案。


<script type=\"text/javascript\"src=\"file1.js\"defer></script>


一個帶有defer屬性的<script>標籤可以放置在文檔的任何位置,對應的JavaScript文件將在<script>被解析時啟動下載,但直到DOM加載完成,也就是在load事件被調用之前代碼不會被執行。

當一個defer的JavaScript文件被下載時,由於它不會阻塞瀏覽器的其他處理過程,所以這些文件可以與頁面的其他資源一起並行下載。

任何帶有defer屬性的<script>元素在DOM加載完成之前不會被執行,不論是內聯腳本還是外部腳本文件。例如,下面代碼展示了defer屬性如何影響腳本行為。


<html>

<head>

<title></title>

</head>

<body>

<script defer>alert(\"defer\");</script>

<script>alert(\"script\");</script>

<script>

window.onload=function{

alert(\"load\");

};

</script>

</body>

</html>


這些代碼在頁面處理過程中彈出3個對話框。如果瀏覽器不支持defer屬性,那麼彈出對話框的順序是defer、script和load。如果瀏覽器支持defer屬性,那麼彈出對話框的順序是script、defer和load。

注意:標記為defer的<script>元素不是跟在第二個<script>後面運行,而是在load事件處理之前被調用。

(2)動態腳本加載

如果用戶瀏覽器只包括IE和Firefox,那麼defer腳本確實有用。如果需要支持跨領域的多種瀏覽器,那麼還有與defer更一致的實現方式:動態腳本加載。動態腳本加載是非阻塞JavaScript下載中最常用的模式,因為它可以跨瀏覽器,而且簡單易用。

文檔對像模型(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、Chrome和Safari都會在<script>節點接收完成之後發出一個load事件,這樣可以監聽<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_r(\"head\")[0].appendChild(script);


IE不支持標籤的load事件,卻支持另一種實現方式,它會發出一個readystatechange事件。<script>元素有一個readyState屬性,它的值隨著下載外部文件的過程而改變。readyState有5種取值:

❑uninitialized,默認狀態。

❑loading,下載開始。

❑loaded,下載完成。

❑interactive,下載完成但尚不可用。

❑complete,所有數據已經準備好。

在<script>元素的生命週期中,readyState的這些取值不一定全部出現,也並沒有指出哪些取值總會被用到。不過在實踐中loaded和complete狀態值很重要。在IE中這兩個readyState值所表示的最終狀態並不一致,有時<script>元素會得到loader,卻從不出現complete,而在另外一些情況下出現complete而用不到loaded。最安全的辦法就是在readystatechange事件中檢查這兩種狀態,並且當其中一種狀態出現時,刪除readystatechange事件句柄,保證事件不會被處理兩次。


var script=document.createElement(\"script\")

script.type=\"text/javascript\";

script.onreadystatechange=function{//IE

if(script.readyState==\"loaded\"||script.readyState==\"complete\"){

script.onreadystatechange=null;

alert(\"Script loaded.\");

}

};

script.src=\"file1.js\";

document.getElementsByTagName_r(\"head\")[0].appendChild(script);


下面的函數封裝了標準實現和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{//其他瀏覽器

script.onload=function{

callback;

};

}

script.src=url;

document.getElementsByTagName_r(\"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。雖然此方法可行,但是如果要下載和執行的文件很多,還是有些麻煩。如果多個文件的次序十分重要,那麼更好的辦法是將這些文件按照正確的次序連接成一個文件。獨立文件可以一次性下載所有代碼,由於這是異步執行,因此使用一個大文件並沒有什麼損失。