异步加载脚本保持执行顺序
首先是外部脚本和行内脚本,对于异步加载的脚本,会导致竞争状态,使得出现未定义的错。
采用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):