加載和執行
腳本阻塞
JavaScript語言是單線程的,意味着同一時間隻能做一件事。
其主要原因是因為JavaScript作為浏覽器腳本語言,它的主要功能是與使用者互動以及操作DOM元素。如果JavaScript采用多線程就會出現一個很嚴重的問題,假設有兩個線程同時運作,一個線程在删除DOM元素,而另一個線程在添加DOM元素,此時浏覽器該以哪一個線程為主?
這就意味着每當有< script>标簽出現時,就會讓頁面等待腳本檔案的加載和執行,無論< script>是内嵌或外鍊形式,其中的原因是因為浏覽器無法預知腳本中是否含有修改DOM樹的操作。
腳本位置
推薦将< script>标簽盡可能的放置在< body>标簽的尾部。
雖然目前浏覽器基本執行并行下載下傳JavaScript檔案,但仍然會阻塞其他資源檔案的下載下傳,而這也就意味着頁面仍然會等待腳本檔案的加載和執行後才能繼續執行。
當浏覽器執行到< body>标簽的尾部時頁面基本已經渲染完全,但當腳本位置出現在文檔頭部時會導緻頁面在加載與執行腳本代碼期間頁面内容為空白,腳本位置出現在文檔中部會導緻頁面渲染會卡住,這兩種情況無論哪一種出現無疑都會造成極差的使用者體驗。
無阻塞腳本
無阻塞腳本的秘訣在window對象的load事件觸發後在開始下載下傳腳本,也就是說在頁面加載完成後才加載JavaScript代碼。
有以下幾種方式可以實作這個功能。
1.defer屬性
在html4中加入了一個擴充屬性:defer。
當一個帶有defer屬性的JavaScript檔案下載下傳時,它可以并行下載下傳此頁面的其他資源檔案,不會阻塞浏覽器的其他程序。
它的使用前提是,此腳本代碼中不含有更改DOM元素的代碼。
帶有defer屬性的< script>标簽可以放置在文檔的任意位置,頁面解析到< script>标簽時開始下載下傳,但并不會執行,直到DOM加載完成(onload事件被觸發)。
以下代碼在不支援defer屬性的情況下執行順序為defer、script、load, 在支援defer屬性的情況下執行順序為script、defer、load。
注意:帶有defer屬性的< script>元素不是跟在第二個後面執行的,而是在onload事件執行之前被調用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./加載和執行.js" defer></script>
<!--alter("defer");defer屬性僅适用于外部腳本 -->
<script>
alert("script");
</script>
<script>
window.onload = () => alert("load");
/*相當于
window.onload = function(){
alert("load");
}
*/
</script>
</body>
</html>
2.async屬性
在html5中加入了一個新擴充屬性:async。
async屬性同樣可以異步加載JavaScript腳本,但與defer差別在于它們的執行時間有差異,defer在頁面完成後才執行,async則是在下載下傳完成後立即執行,而且async的執行并沒有嚴格按照頁面中script的順序而是誰先加載完就先執行誰。
與defer相同,async同樣僅适用于外鍊腳本。
3. 動态腳本元素
由于文檔對象模型(DOM)存在,< script>标簽這個與其他元素并無差異的标簽也可以通過DOM進行引用,都可以在文檔中移動、删除、或者是被建立。
在除IE以外的浏覽器,< script>元素接收完成後會觸發一個load事件。是以我們可以監聽這個事件來擷取腳本完成時的狀态。
var script = document.createElement("script");
script.type = "text/javascript";
script.onload = () => alert("script loaded");
script.src = "./加載和執行.js"; //alert("加載與執行");
document.head.appendChild(script);
/*
由于load事件是在加載完成時才會觸發,是以以上代碼的執行順序為:
加載與執行
script loaded
*/
在IE浏覽器中,會觸發一個readystatechange事件。< script>元素提供了一個readystate屬性,該屬性有五個取值供我們判斷加載過程中的不同階段。
屬性名 | 解釋 |
---|---|
uninitialized | 初始狀态 |
loading | 開始下載下傳 |
loaded | 下載下傳完成 |
interactive | 資料下載下傳完成但尚不可用 |
complete | 所有資料準備就緒 |
在實際應用中,最常用的兩個狀态時"loaded"和"complete"。在IE中使用readystatechange事件最靠譜的方式是同時檢查這兩種狀态,因為有時會到達"loaded"狀态而不到達"complete"狀态,又是甚至不經過"loaded"狀态直接到達"complete"狀态,是以我們最好在其中一個狀态觸發時就删除掉事件處理器以確定事件不會被觸發兩次。
var script = document.createElement("script");
script.type = "text/javascript";
//IE
script.onreaystatechange = () => {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreaystatechange = null;
alert("script loaded");
}
}
script.src = "./加載和執行.js";
document.head.appendChild(script);
為了保證浏覽器相容性,以及友善我們調用是以把它們封裝成一個方法。
//調用格式
loadScript("./加載和執行.js", function() {
alert("file is loaded");
});
//封裝方法
function loadScript(url, callback) {
var script = document.createElement("script");
script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = () => {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
}
} else { //其他浏覽器
script.onload = () => callback();
}
script.src = url;
document.head.appendChild(script);
}
動态腳本加載憑借它的跨浏覽器相容性和易用的優勢,成為最通用的無阻塞加載解決方法。
4. XMLHttpRequest腳本注入
此技術會先建立一個XHR對象,然後用它下載下傳JavaScript檔案,最後通過建立動态< script>元素将代碼注入頁面。
var xhr = new XMLHttpRequest();
xhr.open("get", "加載和執行.js", "true");
xhr.readyStatechange = function() {
if (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);
這段代碼使用了AJAX同源的方式擷取了檔案,當收到有效響應時就會建立一個< script>元素。這個方式的優點就是下載下傳JavaScript代碼後不會立即執行,你可以把你的腳本的執行推遲到你準備好的時候,而且不需要考慮浏覽器的相容性。
但缺點也很明顯,它是同源的,不能跨域,是以基本不會使用這個技術。