今天有人问我,163邮箱那样的Javascript加载进度条是如何实现的。
我不知道,不过实现一个不难,因为<script />有onload和onreadystatechange。还有就是,我们有Atlas。
Atlas中有个类:Sys.ScriptLoader,它的作用就是在页面中依次地加载多个Script文件。在实现之前,先来分析一下这个类的代码。
1 Sys.ScriptLoader = function () { 2 3 // 所有Script的reference对象数组。 4 var _references; 5 // 所有Script加载完之后执行的回调函数。 6 var _completionCallback; 7 // 执行回调函数时提供的上下文(参数)。 8 var _callbackContext; 9 10 // 当前正在加载的Script的HTTP Element(<script />)。 11 var _currentLoadingReference; 12 // 当前的Script加载完成后所调用的回调函数。 13 var _currentOnScriptLoad; 14 15 // ScriptLoader唯一的方法,传入三个参数,参数含义不再赘述。 16 this.load = function(references, completionCallback, callbackContext) { 17 _references = references; 18 _completionCallback = completionCallback; 19 _callbackContext = callbackContext; 20 21 loadReferences(); 22 } 23 24 // 开始加载引用。 25 function loadReferences() { 26 // 如果当前正在加载某个Script。 27 // 这表示此方法不是第一次被调用,而是在某个Script被加载 28 // 完成后才被调用,用以加载下一个Script。 29 if (_currentLoadingReference) { 30 // 查看当前Script元素的readyState,IE下为complete, 31 // 其他浏览器如FF则为loaded(FF其实并无此属性, 32 // 但是下面的代码会将其设为loaded)。 33 // 如果加载失败,则退出。 34 if ((_currentLoadingReference.readyState != 'loaded') && 35 (_currentLoadingReference.readyState != 'complete')) { 36 return; 37 } 38 else { 39 // 进入此分支,表明加载成功。 40 41 // 如果当前Script定义了onLoad函数。 42 if (_currentOnScriptLoad) { 43 // 通过eval调用(这里是个麻烦的地方)。 44 eval(_currentOnScriptLoad); 45 // 设为null,释放资源。 46 _currentOnScriptLoad = null; 47 } 48 49 // 将相关事件设为null以确保释放资源。 50 if (Sys.Runtime.get_hostType() != Sys.HostType.InternetExplorer) { 51 // 如果当前浏览器不是IE,见下面的代码 52 // 会发现为<script />定义了onload事件。 53 _currentLoadingReference.onload = null; 54 } 55 else { 56 // 如果是IE,见下面代码会发现为了 57 // <script />定义了onreadystatechange事件。 58 _currentLoadingReference.onreadystatechange = null; 59 } 60 61 // 最终释放当前的<script />引用。 62 _currentLoadingReference = null; 63 } 64 } 65 66 // 如果还有没有加载的Script。 67 if (_references.length) { 68 // 出队列。 69 var reference = _references.dequeue(); 70 // 创建<script /> 71 var scriptElement = document.createElement('script'); 72 // 设当前的<script />和当前加载成功的回调函数。 73 _currentLoadingReference = scriptElement; 74 _currentOnScriptLoad = reference.onscriptload; 75 76 if (Sys.Runtime.get_hostType() != Sys.HostType.InternetExplorer) { 77 // 如果不是IE的话,那么为<script />设属性readyState, 78 // 并且使用onload事件。 79 scriptElement.readyState = 'loaded'; 80 scriptElement.onload = loadReferences; 81 } 82 else { 83 // 如果是IE,那么使用onreadystatechange事件。 84 scriptElement.onreadystatechange = loadReferences; 85 } 86 scriptElement.type = 'text/javascript'; 87 scriptElement.src = reference.url; 88 89 // 将<script />添加至DOM 90 var headElement = document.getElementsByTagName('head')[0]; 91 headElement.appendChild(scriptElement); 92 93 return; 94 } 95 96 // 如果执行到这里,说明所有的Script已经加载完了。 97 // 如果定义了所有Script加载完之后执行的回调函数, 98 // 那么执行并释放资源。 99 if (_completionCallback) { 100 var completionCallback = _completionCallback;101 var callbackContext = _callbackContext;102 103 _completionCallback = null;104 _callbackContext = null;105 106 completionCallback(callbackContext);107 }108 109 _references = null;110 }111} 112 Sys.ScriptLoader.registerClass('Sys.ScriptLoader'); 可以看出,Sys.ScriptLoader加载script的方法就是通过代码依次向<header />里添加<script />元素。事实上,它在Atlas中被使用的非常少。
事实上,Sys.ScriptLoader的代码非常简单,我添加的注释越看越像画蛇添足。值得注意的是所有的资源都被尽可能的释放。尤其注意从第99 行开始的代码,if体内首先用临时变量保留两个全局变量,然后再将全局变量释放。其目的就是避免在completionCallback在执行时抛出异常 而导致的内存泄露,即使只有万分之一的可能性。Javascript越多,则越容易造成内存泄露,在编写JS代码时最好注意这方面的问题。
接着解释一下load方法的第一个参数references,原本以为这一个Sys.Reference类的数组,结果发现其实相差甚远。不管怎么样顺便看一下该类的代码。
1 Sys.Reference = function () { 2 3 var _component; 4 var _onload; 5 6 this.get_component = function() { 7 return _component; 8 } 9 this.set_component = function(value) { 10 _component = value;11 }12 13 this.get_onscriptload = function() { 14 return _onload;15 }16 this.set_onscriptload = function(value) { 17 _onload = value;18 }19 20 this.dispose = function() { 21 _component = null;22 }23 24 this.getDescriptor = function() { 25 var td = new Sys.TypeDescriptor();26 27 td.addProperty('component', Object);28 td.addProperty('onscriptload', String);29 return td;30 }31} 32 Sys.Reference.registerSealedClass('Sys.Reference', null , Sys.ITypeDescriptorProvider, Sys.IDisposable); 33 Sys.TypeDescriptor.addType('script', 'reference', Sys.Reference); 关心一下Sys.ScriptLoader类的代码可知,reference数组的每个元素其实只是简单的“{ url : " ", onscriptload : "alert(1)"}”形式的对象。不过这样也好,想构造这么一个数组也能轻易地使用JSON了。
到这里,我想大家也应该想到了如何使用Sys.ScriptLoader轻而易举地制作JS加载的进度条。不过既然写到了这里,也就继续把它进行一个简单的实现。
首先是aspx文件。
1 <% @ Page Language="C#" %> 2 3 <! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" > 4 5 < script runat ="server" > 6 7 </ script > 8 9 < html xmlns ="http://www.w3.org/1999/xhtml" > 10 < head runat ="server" > 11 < title > Load Scripts </ title > 12 < script language ="javascript" > 13 function Load()14 { 15 document.getElementById("bar").style.width = "0px";16 var scripts = new Array();17 for (var i = 0; i < 8; i++)18 { 19 var s = new Object();20 var sleep = Math.round((Math.random() * 400)) + 100;21 s.url = "Script.ashx?sleep=" + sleep + "&t=" + Math.random();22 s.cost = sleep;23 scripts.push(s);24 }25 26 Jeffz.Sample.LoadScripts.load(scripts);27 }28 </ script > 29 </ head > 30 < body style ="font-family: Arial;" > 31 < form id ="form1" runat ="server" > 32 < div > 33 < atlas:ScriptManager ID ="ScriptManager1" runat ="server" > 34 < Scripts > 35 <atlas:ScriptReference Path="js/LoadScripts.js" />36 </ Scripts > 37 </ atlas:ScriptManager > 38 39 Progress Bar: 40 < div style ="border: solid 1px black;" > 41 < div id ="bar" style ="height: 20px; width:0%; background-color:Red;" ></ div > 42 </ div > 43 < input type ="button" onclick ="Load()" value ="Load" /> 44 < div id ="message" ></ div > 45 </ div > 46 </ form > 47 </ body > 48 </ html > 非常的简单。使用两个DIV制作了一个最简单的进度条。在点击按钮时调用了Load()函数。该函数随机生成了Script链接并生成了一个8元素的scripts数组。scripts数组的格式如下:
1 var scripts = 2 [ 3 { url : "[url]http://www.sample.com/sample1.js[/url]", cost : costOfLoading1 } , 4 { url : "[url]http://www.sample.com/sample2.js[/url]", cost : costOfLoading2 } , 5 { url : "[url]http://www.sample.com/sample3.js[/url]", cost : costOfLoading3 } 6 ]; 每个元素的url属性不必说,而cost的功能就是表示加载该文件所消耗的时间的 值。这个值没有单位,用到的只是这个值在总共消耗里的比例。另外,可以看到有一个Script.ashx,其作用是模拟一个长时间script加载,它会 根据querystring中的sleep的值将线程休眠一段时间(至于后面的t,目的只是通过改变querystring来避免点击按钮时浏览器的缓 存),这个文件几乎没有代码,可以在范例下载中看到它的实现。最后通过调用Jeffz.Sample.LoadScripts.load方法进行加载,这 就涉及到了下面的代码,LoadScripts.js:
1 Type.registerNamespace('Jeffz.Sample'); 2 3 Jeffz.Sample.LoadScripts = new function () 4 { 5 var totalCost = 0; 6 var scriptLoader = new Sys.ScriptLoader(); 7 8 this.load = function(scripts) 9 { 10 if (Jeffz.Sample.__onScriptLoad != null)11 { 12 throw new Error("In progress");13 }14 15 totalCost = 0;16 Jeffz.Sample.__onScriptLoad = onScriptLoad;17 var references = new Array();18 19 var loadedCost = 0;20 for (var i = 0; i < scripts.length; i++)21 { 22 totalCost += scripts[i].cost;23 loadedCost += scripts[i].cost;24 25 var ref = createReference(scripts[i].url, loadedCost);26 27 references.push(ref);28 }29 30 scriptLoader.load(references, onComplete);31 }32 33 function createReference(url, loadedCost)34 { 35 var ref = new Object();36 ref.url = url;37 ref.onscriptload = "Jeffz.Sample.__onScriptLoad('" + url + "', " + loadedCost + ")";38 return ref;39 }40 41 function onComplete()42 { 43 Jeffz.Sample.__onScriptLoad = null;44 }45 46 function onScriptLoad(url, loadedCost)47 { 48 var progress = 100.0 * loadedCost / totalCost;49 document.getElementById("bar").style.width = progress + "%";50 document.getElementById("message").innerHTML += ("<strong>" + url + "</strong>" + " loaded.<br />");51 }52} 哎,似乎完全没有必要对代码进行多余的解释。到目前为止,一个简单的Script加载进度条就完成了,相当的简单。代码可以 ,也可以 。 不过事情到此为止了吗?事实上,我对这个Solution不怎么满意,虽然对于大多数情况应该已经够用了。可以注意到,我将 Jeffz.Sample.LoadScripts实现成为了一个Singleton,也就是说,没有另外一个和它一样的实例。并且在load方法的一开 始就判断是不是正在加载,如果是,那么会抛出一个异常。实现了这么一种“单线程”的加载,直接原因是受限于Sys.ScriptLoader的实现。 请看Sys.ScriptLoader代码的第44行,它使用了eval来“邪恶”地进行了script加载完成时的回调。这其实对于开发人员是一种非 常难受的实现,因为eval,所以无法地将一个函数的引用作为回调函数来传递。唯一能做的就是只能把“根代码”作为字符串形式来交给 Sys.ScriptLoader。虽然还是能够通过Sys.ScriptLoader实现“并发”的Script加载(说白了最多像 Sys.ScriptLoader一样建一个队列嘛),但是代码量自然而然就上去了,开发的复杂度也提高了。 另外,Sys.ScriptLoader在加载某Script出错时也没有提示,而是直接退出,这个也不是很理想。 不过我认为,这种“单线程”的script加载已经足够用于大多数情况了。而且如果真的有“特殊”要求,参照Sys.ScriptLoader这个如此清晰明了的范例,自己重新写一个对于广大开发人员来说,难道还不是易如反掌的事情吗?
本文转自 jeffz 51CTO博客,原文链接:http://blog.51cto.com/jeffz/60960,如需转载请自行联系原作者