瀏覽器是如何加載JS的
當瀏覽器遇到一個<script>標籤時,瀏覽器首先根據標籤src屬性下載JavaScript代碼,而後運行JavaScript代碼,繼而繼續解析和翻譯頁面。若是須要加載的js文件不少很大,則會讓人感受頁面加載很慢,影響頁面的交互。瀏覽器在遇到<body>以前,不會渲染頁面的任何部分,若是此時<head>中須要加載的js文件很大的話,可能用戶開始看到的頁面就是一個「白板」,這種狀況會立馬讓用戶崩潰。
Internet Explorer 8, Firefox 3.5, Safari 4, 和Chrome 2 容許並行下載JavaScript 文件。這代表當一個script文件正在下載時,不會阻塞其餘script的下載。並行下載script加快了script的下載時間,但仍是要阻塞其餘資源的下載,如圖片。
解決方案
1.將腳本放在頁面底部
一種常見的作法就是將<script>標籤放在閉合的</body>以前。這樣就能夠先把頁面展現給用戶,讓頁面的加載速度顯得不是很慢。這時最好將CSS文件放在head中,一邊加載DOM一邊渲染樣式。
2.成組下載腳本
咱們知道多個HTTP請求也會下降頁面性能。所以咱們能夠採用將多個腳本文件合併到一個文件下載。這雖然減小了HTTP請求,但這給咱們增長了較大的額外工做,由於每次發佈咱們都要合併文件。爲此,咱們能夠採用成組下載的方式來達到目的。成組下載就是一次請求下載多個腳本文件。例如如下的URL:
每次向服務器的固定服務請求下載多個文件。服務器將多個文件合併一塊兒返回給客戶端。這是HTML頁面包含多個外部JavaScript的最佳方法。
3.延時加載
若是script文件又大又多,那麼恐怕咱們怎麼壓縮文件怎麼減小http請求數,script的加載都會鎖定瀏覽器一大段時間。這時比較好的方法就是等頁面加載完成後再加載script,也就是在window.onload事件發出以後開始下載代碼。
HTML4中的defer屬性能夠實現下載script時不阻塞瀏覽器其餘處理過程,代碼下載完成後也不會被執行,而是等到DOM加載完成後(window.onload以前)才被執行。但defer屬性只被IE4+和FF3.5+支持,其餘瀏覽器不支持該屬性,所以這不是一個理想的跨瀏覽器解決方案。
延時加載的一種通用方法就是動態建立腳本元素。一個新的<script>元素能夠很是容易地經過標準DOM函數建立:
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "xxx.js";
document.getElementsByTagName("head")[0].appendChild(script);
當script元素被添加到頁面以後便開始下載腳本文件。該技術的優勢是:不管在何處啓動下載,文件的下載和運行都不會阻塞其餘頁面處理過程。當script文件下載完成後,返回的代碼一般被當即執行(除了FF和Opera,它們將等待此前的全部動態腳本節點執行完畢。)
當動態加載的script只是其餘script調用的接口時,就會出現問題。由於調用代碼不知道被調用的接口是否已準備完畢。不過還好,目前主流瀏覽器都可以跟蹤到節點是否加載完成。
FF, Opera, Chrome和Safari3+會在節點接收完成後發出一個load事件;IE則是發出一個readystatechange事件,<script>元素有一個readyState屬性,它的值隨着下載過程而改變。readyState有5種取值:uninitialized(默認狀態),loading(下載開始),loaded(下載完成),interactive(下載完成但尚不可用),complete(全部數據已準備好)。微軟文檔上說,這些取值不必定所有出現,有時script會獲得loaded不出現complete,有時script會獲得complete不出現loaded。最安全的作法就是,在readystatechange事件中檢查這兩種狀態,當出現兩種狀態之一時,刪除readystatechange句柄,以免事件不會被執行兩次。
根據上面的描述,新的動態加載代碼以下所示:
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{ // FF, Chrome, Opera, ...
script.onload = function(){
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
若是要加載多個文件,而且要保證順序,則能夠採用將上述函數串聯的方式實現。
請看下面的使用示例:
loadScript.js:實現loadScript函數
頁面文件:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>動態加載</title>
<script type="text/javascript" src="../scripts/loadScript.js"></script>
<script type="text/javascript">
loadScript("../scripts/test1.js", function(){
loadScript("../scripts/test2.js", function(){
loadScript("../scripts/test3.js", function(){
test3.print("hi, i'm abc.");
})
});
});
</script>
</head>
<body>
</body>
用於測試的test1.js:
用於測試的test2.js,test2依賴於test1:
var test2 = {
print: function(msg){
test1.print(msg);
alert("from test2: " + msg);
}
};
用於測試的test3.js,test3依賴於test2:
var test3 = {
print: function(msg){
test2.print(msg);
alert("from test3: " + msg);
}
};
運行頁面,將前後彈出「from test1: hi, i'm abc.」, "from test2: hi, i'm abc.", "from test3: hi, i'm abc."。
動態腳本加載是最經常使用的JavaScript非阻塞下載方式,由於它跨瀏覽器並且簡單易用。
XMLHttpRequest腳本注入
利用XHR對象下載JavaScript代碼,而後動態建立script元素將JavaScript代碼注入到頁面中。
這種方法須要用到ajax,先把ajax的輔助方法列出來:
/**
* create xmlhelper
* */
(function(window){
var xhrhelper = (function(){
var xhrhelper = function(){};
xhrhelper.extend = function(){
var len = arguments.length, target = this;
for(var i = 0; i < len; i++){
var source = arguments[i];
for(var p in source){
if(target[p] == undefined){
target[p] = source[p];
}else{
throw new Error("extend error: "+ p +" is already existed.");
}
}
}
};
return xhrhelper;
})();
window.xhrhelper = xhrhelper;
})(window);
/**
* extend xmlhelper
* */
xhrhelper.extend({
/**
* create XmlHttpRequest object.
* */
createXmlHttp: function(){
var xmlhttp;
/*@cc_on
@if (@_jscript_version >= 5)
try{
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e){
try{
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch(E){
xmlhttp = false;
}
}
@else
xmlhttp = false;
@end
@*/
if(!xmlhttp && typeof XMLHttpRequest != 'undefined'){
try{
xmlhttp = new XMLHttpRequest();
} catch(e){
xmlhttp = false;
}
}
return xmlhttp;
},
/**
* send request.
* */
req: function(options){
var defaults = {
method: "get", // GET,POST,PUT,DELTE
url: null,
data: null,
async: true,
username: null,
password: null,
onloading: function(xhr){},
onloaded: function(xhr){},
oninteractive: function(xhr){},
oncomplete: function(xhr){},
onabort: function(xhr){},
onerror: function(xhr){}
};
for(var p in options){
defaults[p] = options[p];
}
var xmlhttp = xhrhelper.createXmlHttp();
if(xmlhttp){
xmlhttp.onreadystatechange = function(){
if(xmlhttp.readyState == 1){ // 未初始化
defaults.onloading(xmlhttp);
} else if(xmlhttp.readyState == 2){ // 已初始化
defaults.onloaded(xmlhttp);
} else if(xmlhttp.readyState == 3){
defaults.oninteractive(xmlhttp);
} else if(xmlhttp.readyState == 4){
if(xmlhttp.status == 0){
defaults.onabort(xmlhttp);
} else if(xmlhttp.status >= 200 && xmlhttp.status <= 300 || xmlhttp.status == 304){
defaults.oncomplete(xmlhttp);
} else{
defaults.onerror(xmlhttp);
}
}
};
if(defaults.username != null && defaults.password != null &&
defaults.username.length != 0 && defaults.password.length != 0){
xmlhttp.open(defaults.method, defaults.url, defaults.async,
defaults.username, defaults.password);
}else{
xmlhttp.open(defaults.method, defaults.url, defaults.async);
}
if(defaults.method.toLowerCase() == "get"){
}else if(defaults.method.toLowerCase() == "post"){
xmlhttp.setRequestHeader('Content-Type', 'text/html; charset=UTF-8');
}
xmlhttp.send(defaults.data);
}
return xmlhttp;
}
});
而後是利用xhr下載js文件的函數:
function xhrload(url, callback){
xhrhelper.req({
method: "get",
url: url,
oncomplete: function(xhr){
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
callback();
}
});
}
在頁面中能夠經過以下方式調用xhrload函數:
xhrload("../scripts/test1.js", function(){
test1.print("ABC");
});
這種方法的優勢是:能夠下載不當即執行的JavaScript代碼。因爲代碼下載在了<script>標籤以外,它下載後不當即執行。另外一個優勢是幾乎全部的瀏覽器都支持這種方法。
這種方法的限制是:JavaScript文件必須與頁面放在同一個域內。由於XmlHttpRequest不能跨域訪問。
第三方庫
YUI
LazyLoad
LABjs