首先是外部腳本和行內腳本,對於異步加載的腳本,會導致競爭狀態,使得出現未定義的錯。
采用Script Dom技術測試:
代碼:
<script type="text/javascript"> var scriptElem = document.createElement('script'); scriptElem.src = "js/jquery-2.1.1.js"; document.getElementsByTagName('head')[0].appendChild(scriptElem); </script> <script type="text/javascript"> function test(){ $("#test").addClass('class_name'); } test(); </script>
運行結果:
以下幾種方式解決該問題:
1.硬編碼回調
將test方法的執行定義在外部腳本(即調用的腳本),該方法不靈活,如果調用的是第三方腳本的話,更加麻煩。此處不顯示例子。
2.Window onload:
通過監聽window的onload事件來觸發行內代碼的執行。只要確保外部腳本在window。Onload之前下載執行就可以保持執行順序。
運行結果:
代碼:
function test(){ $("#test").addClass('class_name'); } if(window.addEventListener){ window.addEventListener("load", test, false); }else if(window.attachEvent){ window.attachEvent("onload",test); }
缺點:1.必須確保異步腳本是通過阻塞onload事件的方式加載的。
2.如果頁面有更多的資源,那么外部腳本可能在onload時間出發之前早就完成加載,一般來說,行內腳本最好在外部腳本下載和執行完成之后立即調用。
3.定時器:
采用輪詢方法來抱着在行內腳本執行之前所依賴的外部腳本已經加載。
運行結果:
代碼:
function test(){ $("#test").addClass('class_name'); console.log(index); } var index = 0; function initTimer(){ if("undefined" === typeof($)){ index++; setTimeout(initTimer,300) }else{ test(); } } initTimer();
缺點:如果在setTimeout方法中設置的時間太小,會造成額外的開銷。設置太大會導致和windon.onload的方法一樣,腳本加載完成無法立即執行行內腳本。另外,如果腳本出錯,輪詢會無限進行下去。
4.Script onload:
前面提到的整合技術會增加頁面的脆弱性、延遲和開銷,通過監聽腳本的onload事件可以解決這些問題。
運行結果:
代碼:
function test(){ $("#test").addClass('class_name'); } var scriptElem = document.createElement('script'); scriptElem.src = "js/jquery-2.1.1.js"; scriptElem.onloadDone = false; scriptElem.onload = function(){ scriptElem.onloadDone = true; test(); } scriptElem.onreadyStatechange = function(){ if(("loaded" === scriptElem.readyState || "complete" === scriptElem.readyState) && !scriptElem.onloadDone){ scriptElem.onloadDone = true; test(); } } document.getElementsByTagName('head')[0].appendChild(scriptElem);
優點:維護簡單,事件處理也簡單,整合異步加載外部腳本和行內腳本的首選。
5.降級使用script標簽:
即用一個標簽即包含外部腳本,又使用行內腳本,如下:
<script src=" js/jquery-2.1.1.js " > function test(){ $("#test").addClass('class_name'); } </script>
由於瀏覽器並不支持這種模式,所以需要在腳本的內部增加代碼來執行行內腳本,找到該腳本,並用eval執行其內容。如下:
var scripts = document.getElementsByTagName('script'); var cntr = scripts.length; while(cntr--){ var curScript = scripts[cntr-1]; if(-1 != curScript.src.indexOf("js/jquery-2.1.1.js")){ eval(curScript.innerHTML); break; } cntr--; }
此處不給出例圖,具體代碼如下:
function test(){ $("#test").addClass('class_name'); } var scriptElem = document.createElement('script'); scriptElem.src = "js/jquery-2.1.1.js"; if(-1 != navigator.userAgent.indexOf("Opera")){ scriptElem.innerHTML = "test()"; }else{ scriptElem.text = "test()"; } document.getElementsByTagName('head')[0].appendChild(scriptElem);
優點:技術優雅簡潔,開銷最小。
缺點:需要修改外部腳本,對第三方庫不適用。
多個腳本按序執行:
正常引入腳本:
運行結果:
采用XHR eval:
運行結果:
由於腳本沒有按順序執行,出現未定義的錯誤。
解決方法1:Managed XHR
通過EFWS.Script模塊封裝了一種技術,將XHR響應加入隊列來保證它們按順序執行。
代碼:
/* 數組queuedScripts存儲執行隊列中的腳本,每個腳本是擁有三個屬性的對象: response: XHR響應 onload: 腳本加載后觸發的函數 bOrder: 如果該腳本需要依賴其他腳本按順序執行,則設為true */ EFWS.Script = { queuedScripts: [], //傳入三個參數,第二個參數可選 loadScriptXhrInjection: function(url, onload, bOrder) { var iQ = EFWS.Script.queuedScripts.length; //如果需要按順序執行,並將腳本對象放入數組 if (bOrder) { var qScript = {response: null, onload: onload, done: false}; EFWS.Script.queuedScripts[iQ] = qScript; } //調用AJAX var xhrObj = EFWS.Script.getXHRObject(); xhrObj.onreadystatechange = function() { if (xhrObj.readyState == 4) { //如果第三個參數的值為true,即調用injectScripts()函數 if (bOrder) { EFWS.Script.queuedScripts[iQ].response = xhrObj.responseText; EFWS.Script.injectScripts(); //如果不需要按順序執行,即立即加載腳本 } else { eval(xhrObj.responseText); if (onload) { onload(); } } } }; xhrObj.open('GET', url, true); xhrObj.send(''); }, //遍歷數組,當發現某一腳本加載但未執行時,立即執行 injectScripts: function() { var len = EFWS.Script.queuedScripts.length; for (var i = 0; i < len; i++) { var qScript = EFWS.Script.queuedScripts[i]; //已加載的腳本 if (!qScript.done) { //如果響應未返回 立即停止 if (!qScript.response) { break; //執行腳本 } else { eval(qScript.response); if (qScript.onload) { qScript.onload(); } qScript.done = true; } } } }, //AJAX對象 getXHRObject: function() { var xhrObj = false; try { xhrObj = new XMLHttpRequest(); } catch(e) { var aTypes = ["Msxm12.XMLHTTP6.0", "Msxm12.XMLHTTP3.0", "Msxm12.XMLHTTP", "Microsoft.XMLHTTP"]; var len = aTypes.length; for (var i = 0; i < len; i++) { try { xhrObj = new ActiveXObject(aTypes[i]); } catch(e) { continue; } break; } } finally { return xhrObj; } } }; //調用腳本 EFWS.Script.loadScriptXhrInjection("js/jquery-2.1.1.js", null, true); EFWS.Script.loadScriptXhrInjection("js/first.js", null, true); EFWS.Script.loadScriptXhrInjection("js/second.js", null, true);
運行結果:
缺點:必須同域。
當腳本不同域時,可以采用Script Dom Element 和document.write Script Tag的方法。
由於document.write Script Tag在並行下載腳本時會阻塞其他資源,而Script Dom Element則只在FireFox(實際測試FireFox並不行,可能是版本原因)和Opeare按序執行,所以應在不同瀏覽器采用不同方法。
代碼:
var ScriptLoader ={}; ScriptLoader.script = { loadScriptDomElement:function(url, onload){ 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; if(onload) onload(); } }; } else { //Others script.onload = function(){ if(onload) onload(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }, loadScriptDomWrite: function(url,onload){ document.write('<script src="'+url+'" type="text/javascript"></scr'+'ipt>'); if(onload){ if(elem.addEventListener){//others elem.addEventListener(window,'load',onload); }else if(elem.attachEvent){ //IE elem.addEventListener(window,'onload',onload); } } }, //根據瀏覽器選擇瀏覽器加載js的方式 loadScript: function(url,onload){ if( -1 != navigator.userAgent.indexOf('Opera')){ //當瀏覽器為firefox和opera時通過Script Dom Element 保證腳本執行順序 ScriptLoader.script.loadScriptDomElement(url,onload); }else{ //當為其他瀏覽器時,通過document write Script保證腳本執行順序。此時腳本的加載會阻塞其他資源,這是一種折衷 ScriptLoader.script.loadScriptDomWrite(url,onload); } } } //調用腳本 ScriptLoader.script.loadScript("js/jquery-2.1.1.js", null); ScriptLoader.script.loadScript("js/first.js", null); ScriptLoader.script.loadScript("js/second.js", null);
運行結果(write):