Sys.ScriptLoader与JS加载进度条的实现

简介:
 今天有人问我,163邮箱那样的Javascript加载进度条是如何实现的。

我不知道,不过实现一个不难,因为<script />有onload和onreadystatechange。还有就是,我们有Atlas。

Atlas中有个类:Sys.ScriptLoader,它的作用就是在页面中依次地加载多个Script文件。在实现之前,先来分析一下这个类的代码。

  1 ExpandedBlockStart.gif Sys.ScriptLoader  =   function ()  {
  2InBlock.gif
  3InBlock.gif    // 所有Script的reference对象数组。
  4InBlock.gif    var _references;
  5InBlock.gif    // 所有Script加载完之后执行的回调函数。
  6InBlock.gif    var _completionCallback;
  7InBlock.gif    // 执行回调函数时提供的上下文(参数)。
  8InBlock.gif    var _callbackContext;
  9InBlock.gif
 10InBlock.gif    // 当前正在加载的Script的HTTP Element(<script />)。
 11InBlock.gif    var _currentLoadingReference;
 12InBlock.gif    // 当前的Script加载完成后所调用的回调函数。
 13InBlock.gif    var _currentOnScriptLoad;
 14InBlock.gif    
 15InBlock.gif    // ScriptLoader唯一的方法,传入三个参数,参数含义不再赘述。
 16ExpandedSubBlockStart.gif    this.load = function(references, completionCallback, callbackContext) {
 17InBlock.gif        _references = references;
 18InBlock.gif        _completionCallback = completionCallback;
 19InBlock.gif        _callbackContext = callbackContext;
 20InBlock.gif        
 21InBlock.gif        loadReferences();
 22ExpandedSubBlockEnd.gif    }

 23InBlock.gif
 24InBlock.gif    // 开始加载引用。
 25ExpandedSubBlockStart.gif    function loadReferences() {
 26InBlock.gif        // 如果当前正在加载某个Script。
 27InBlock.gif        // 这表示此方法不是第一次被调用,而是在某个Script被加载
 28InBlock.gif        // 完成后才被调用,用以加载下一个Script。
 29ExpandedSubBlockStart.gif        if (_currentLoadingReference) {
 30InBlock.gif            // 查看当前Script元素的readyState,IE下为complete,
 31InBlock.gif            // 其他浏览器如FF则为loaded(FF其实并无此属性,
 32InBlock.gif            // 但是下面的代码会将其设为loaded)。
 33InBlock.gif            // 如果加载失败,则退出。
 34InBlock.gif            if ((_currentLoadingReference.readyState != 'loaded') &&
 35ExpandedSubBlockStart.gif                (_currentLoadingReference.readyState != 'complete')) {
 36InBlock.gif                return;
 37ExpandedSubBlockEnd.gif            }

 38ExpandedSubBlockStart.gif            else {
 39InBlock.gif                // 进入此分支,表明加载成功。
 40InBlock.gif                
 41InBlock.gif                // 如果当前Script定义了onLoad函数。
 42ExpandedSubBlockStart.gif                if (_currentOnScriptLoad) {
 43InBlock.gif                    // 通过eval调用(这里是个麻烦的地方)。
 44InBlock.gif                    eval(_currentOnScriptLoad);
 45InBlock.gif                    // 设为null,释放资源。
 46InBlock.gif                    _currentOnScriptLoad = null;
 47ExpandedSubBlockEnd.gif                }

 48InBlock.gif                
 49InBlock.gif                // 将相关事件设为null以确保释放资源。
 50ExpandedSubBlockStart.gif                if (Sys.Runtime.get_hostType() != Sys.HostType.InternetExplorer) {
 51InBlock.gif                    // 如果当前浏览器不是IE,见下面的代码
 52InBlock.gif                    // 会发现为<script />定义了onload事件。
 53InBlock.gif                    _currentLoadingReference.onload = null;
 54ExpandedSubBlockEnd.gif                }

 55ExpandedSubBlockStart.gif                else {
 56InBlock.gif                    // 如果是IE,见下面代码会发现为了
 57InBlock.gif                    // <script />定义了onreadystatechange事件。
 58InBlock.gif                    _currentLoadingReference.onreadystatechange = null;
 59ExpandedSubBlockEnd.gif                }

 60InBlock.gif                
 61InBlock.gif                // 最终释放当前的<script />引用。
 62InBlock.gif                _currentLoadingReference = null;
 63ExpandedSubBlockEnd.gif            }

 64ExpandedSubBlockEnd.gif        }

 65InBlock.gif
 66InBlock.gif        // 如果还有没有加载的Script。
 67ExpandedSubBlockStart.gif        if (_references.length) {
 68InBlock.gif            // 出队列。
 69InBlock.gif            var reference = _references.dequeue();
 70InBlock.gif            // 创建<script />
 71InBlock.gif            var scriptElement = document.createElement('script');
 72InBlock.gif            // 设当前的<script />和当前加载成功的回调函数。
 73InBlock.gif            _currentLoadingReference = scriptElement;
 74InBlock.gif            _currentOnScriptLoad = reference.onscriptload;
 75InBlock.gif            
 76ExpandedSubBlockStart.gif            if (Sys.Runtime.get_hostType() != Sys.HostType.InternetExplorer) {
 77InBlock.gif                // 如果不是IE的话,那么为<script />设属性readyState,
 78InBlock.gif                // 并且使用onload事件。
 79InBlock.gif                scriptElement.readyState = 'loaded';
 80InBlock.gif                scriptElement.onload = loadReferences;
 81ExpandedSubBlockEnd.gif            }

 82ExpandedSubBlockStart.gif            else {
 83InBlock.gif                // 如果是IE,那么使用onreadystatechange事件。
 84InBlock.gif                scriptElement.onreadystatechange = loadReferences;
 85ExpandedSubBlockEnd.gif            }

 86InBlock.gif            scriptElement.type = 'text/javascript';
 87InBlock.gif            scriptElement.src = reference.url;
 88InBlock.gif
 89InBlock.gif            // 将<script />添加至DOM
 90InBlock.gif            var headElement = document.getElementsByTagName('head')[0];
 91InBlock.gif            headElement.appendChild(scriptElement);
 92InBlock.gif
 93InBlock.gif            return;
 94ExpandedSubBlockEnd.gif        }

 95InBlock.gif        
 96InBlock.gif        // 如果执行到这里,说明所有的Script已经加载完了。
 97InBlock.gif        // 如果定义了所有Script加载完之后执行的回调函数,
 98InBlock.gif        // 那么执行并释放资源。
 99ExpandedSubBlockStart.gif        if (_completionCallback) {
100InBlock.gif            var completionCallback = _completionCallback;
101InBlock.gif            var callbackContext = _callbackContext;
102InBlock.gif            
103InBlock.gif            _completionCallback = null;
104InBlock.gif            _callbackContext = null;
105InBlock.gif            
106InBlock.gif            completionCallback(callbackContext);
107ExpandedSubBlockEnd.gif        }

108InBlock.gif        
109InBlock.gif        _references = null;
110ExpandedSubBlockEnd.gif    }

111ExpandedBlockEnd.gif}

112 None.gifSys.ScriptLoader.registerClass('Sys.ScriptLoader');
  可以看出,Sys.ScriptLoader加载script的方法就是通过代码依次向<header />里添加<script />元素。事实上,它在Atlas中被使用的非常少。

   事实上,Sys.ScriptLoader的代码非常简单,我添加的注释越看越像画蛇添足。值得注意的是所有的资源都被尽可能的释放。尤其注意从第99 行开始的代码,if体内首先用临时变量保留两个全局变量,然后再将全局变量释放。其目的就是避免在completionCallback在执行时抛出异常 而导致的内存泄露,即使只有万分之一的可能性。Javascript越多,则越容易造成内存泄露,在编写JS代码时最好注意这方面的问题。

接着解释一下load方法的第一个参数references,原本以为这一个Sys.Reference类的数组,结果发现其实相差甚远。不管怎么样顺便看一下该类的代码。

 1 ExpandedBlockStart.gif Sys.Reference  =   function ()  {
 2InBlock.gif
 3InBlock.gif    var _component;
 4InBlock.gif    var _onload;
 5InBlock.gif    
 6ExpandedSubBlockStart.gif    this.get_component = function() {
 7InBlock.gif        return _component;
 8ExpandedSubBlockEnd.gif    }

 9ExpandedSubBlockStart.gif    this.set_component = function(value) {
10InBlock.gif        _component = value;
11ExpandedSubBlockEnd.gif    }

12InBlock.gif    
13ExpandedSubBlockStart.gif    this.get_onscriptload = function() {
14InBlock.gif        return _onload;
15ExpandedSubBlockEnd.gif    }

16ExpandedSubBlockStart.gif    this.set_onscriptload = function(value) {
17InBlock.gif        _onload = value;
18ExpandedSubBlockEnd.gif    }

19InBlock.gif    
20ExpandedSubBlockStart.gif    this.dispose = function() {
21InBlock.gif        _component = null;
22ExpandedSubBlockEnd.gif    }

23InBlock.gif    
24ExpandedSubBlockStart.gif    this.getDescriptor = function() {
25InBlock.gif        var td = new Sys.TypeDescriptor();
26InBlock.gif        
27InBlock.gif        td.addProperty('component', Object);
28InBlock.gif        td.addProperty('onscriptload', String);
29InBlock.gif        return td;
30ExpandedSubBlockEnd.gif    }

31ExpandedBlockEnd.gif}

32 None.gifSys.Reference.registerSealedClass('Sys.Reference',  null , Sys.ITypeDescriptorProvider, Sys.IDisposable);
33 None.gifSys.TypeDescriptor.addType('script', 'reference', Sys.Reference);
  关心一下Sys.ScriptLoader类的代码可知,reference数组的每个元素其实只是简单的“{ url : " [url]http://www.sample.com/sample.js[/url]", onscriptload : "alert(1)"}”形式的对象。不过这样也好,想构造这么一个数组也能轻易地使用JSON了。

到这里,我想大家也应该想到了如何使用Sys.ScriptLoader轻而易举地制作JS加载的进度条。不过既然写到了这里,也就继续把它进行一个简单的实现。

首先是aspx文件。
 1 ExpandedBlockStart.gif <% @ Page Language="C#"  %>
 2 None.gif
 3 None.gif <! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
 4 None.gif
 5 ExpandedBlockStart.gif < script  runat ="server" >
 6ExpandedBlockEnd.gif
 7None.gif
</ script >
 8 None.gif
 9 None.gif < html  xmlns ="http://www.w3.org/1999/xhtml"   >
10 None.gif < head  runat ="server" >
11 None.gif     < title > Load Scripts </ title >
12 ExpandedBlockStart.gif     < script  language ="javascript" >
13InBlock.gif        function Load()
14ExpandedSubBlockStart.gif        {
15InBlock.gif            document.getElementById("bar").style.width = "0px";
16InBlock.gif            var scripts = new Array();
17InBlock.gif            for (var i = 0; i < 8; i++)
18ExpandedSubBlockStart.gif            {
19InBlock.gif                var s = new Object();
20InBlock.gif                var sleep = Math.round((Math.random() * 400)) + 100;
21InBlock.gif                s.url = "Script.ashx?sleep=" + sleep + "&t=" + Math.random();
22InBlock.gif                s.cost = sleep;
23InBlock.gif                scripts.push(s);
24ExpandedSubBlockEnd.gif            }

25InBlock.gif                
26InBlock.gif            Jeffz.Sample.LoadScripts.load(scripts);
27ExpandedSubBlockEnd.gif        }

28ExpandedBlockEnd.gif    
</ script >
29 None.gif </ head >
30 None.gif < body  style ="font-family: Arial;" >
31 None.gif     < form  id ="form1"  runat ="server" >
32 None.gif     < div >
33 None.gif         < atlas:ScriptManager  ID ="ScriptManager1"  runat ="server" >
34 ExpandedBlockStart.gif             < Scripts >
35InBlock.gif                <atlas:ScriptReference Path="js/LoadScripts.js" />
36ExpandedBlockEnd.gif            
</ Scripts >
37 None.gif         </ atlas:ScriptManager >
38 None.gif
39 None.gif        Progress Bar:        
40 None.gif         < div  style ="border: solid 1px black;" >
41 None.gif             < div  id ="bar"  style ="height: 20px; width:0%; background-color:Red;" ></ div >
42 None.gif         </ div >
43 None.gif         < input  type ="button"  onclick ="Load()"  value ="Load"   />
44 None.gif         < div  id ="message" ></ div >
45 None.gif     </ div >
46 None.gif     </ form >
47 None.gif </ body >
48 None.gif </ html >
  非常的简单。使用两个DIV制作了一个最简单的进度条。在点击按钮时调用了Load()函数。该函数随机生成了Script链接并生成了一个8元素的scripts数组。scripts数组的格式如下:
1 None.gif var  scripts  =  
2 None.gif[
3 ExpandedBlockStart.gif     { url : "[url]http://www.sample.com/sample1.js[/url]", cost : costOfLoading1 } ,
4 ExpandedBlockStart.gif     { url : "[url]http://www.sample.com/sample2.js[/url]", cost : costOfLoading2 } ,
5 ExpandedBlockStart.gif     { url : "[url]http://www.sample.com/sample3.js[/url]", cost : costOfLoading3 }
6 None.gif];
  每个元素的url属性不必说,而cost的功能就是表示加载该文件所消耗的时间的 值。这个值没有单位,用到的只是这个值在总共消耗里的比例。另外,可以看到有一个Script.ashx,其作用是模拟一个长时间script加载,它会 根据querystring中的sleep的值将线程休眠一段时间(至于后面的t,目的只是通过改变querystring来避免点击按钮时浏览器的缓 存),这个文件几乎没有代码,可以在范例下载中看到它的实现。最后通过调用Jeffz.Sample.LoadScripts.load方法进行加载,这 就涉及到了下面的代码,LoadScripts.js:
 1 None.gif Type.registerNamespace('Jeffz.Sample');
 2 None.gif
 3 None.gifJeffz.Sample.LoadScripts  =   new   function ()
 4 ExpandedBlockStart.gif {
 5InBlock.gif    var totalCost = 0;
 6InBlock.gif    var scriptLoader = new Sys.ScriptLoader();
 7InBlock.gif
 8InBlock.gif    this.load = function(scripts)
 9ExpandedSubBlockStart.gif    {
10InBlock.gif        if (Jeffz.Sample.__onScriptLoad != null)
11ExpandedSubBlockStart.gif        {
12InBlock.gif            throw new Error("In progress");
13ExpandedSubBlockEnd.gif        }

14InBlock.gif        
15InBlock.gif        totalCost = 0;
16InBlock.gif        Jeffz.Sample.__onScriptLoad = onScriptLoad;
17InBlock.gif        var references = new Array();
18InBlock.gif    
19InBlock.gif        var loadedCost = 0;
20InBlock.gif        for (var i = 0; i < scripts.length; i++)
21ExpandedSubBlockStart.gif        {
22InBlock.gif            totalCost += scripts[i].cost;
23InBlock.gif            loadedCost += scripts[i].cost;
24InBlock.gif            
25InBlock.gif            var ref = createReference(scripts[i].url, loadedCost);
26InBlock.gif            
27InBlock.gif            references.push(ref);
28ExpandedSubBlockEnd.gif        }

29InBlock.gif        
30InBlock.gif        scriptLoader.load(references, onComplete);
31ExpandedSubBlockEnd.gif    }

32InBlock.gif    
33InBlock.gif    function createReference(url, loadedCost)
34ExpandedSubBlockStart.gif    {
35InBlock.gif        var ref = new Object();
36InBlock.gif        ref.url = url;
37InBlock.gif        ref.onscriptload = "Jeffz.Sample.__onScriptLoad('" + url + "', " + loadedCost + ")";
38InBlock.gif        return ref;
39ExpandedSubBlockEnd.gif    }

40InBlock.gif    
41InBlock.gif    function onComplete()
42ExpandedSubBlockStart.gif    {
43InBlock.gif        Jeffz.Sample.__onScriptLoad = null;
44ExpandedSubBlockEnd.gif    }

45InBlock.gif    
46InBlock.gif    function onScriptLoad(url, loadedCost)
47ExpandedSubBlockStart.gif    {
48InBlock.gif        var progress = 100.0 * loadedCost / totalCost;
49InBlock.gif        document.getElementById("bar").style.width = progress + "%";
50InBlock.gif        document.getElementById("message").innerHTML += ("<strong>" + url + "</strong>" + " loaded.<br />");
51ExpandedSubBlockEnd.gif    }

52ExpandedBlockEnd.gif}
  哎,似乎完全没有必要对代码进行多余的解释。到目前为止,一个简单的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,如需转载请自行联系原作者

相关文章
|
3月前
|
JavaScript 前端开发 Java
面试官:你的项目有什么亮点?我:解决了JS脚本加载失败的问题!
面试官:你的项目有什么亮点?我:解决了JS脚本加载失败的问题!
|
7月前
|
自然语言处理 前端开发 JavaScript
使用 Promise 来改写 JavaScript 的加载逻辑
使用 Promise 来改写 JavaScript 的加载逻辑
57 0
|
7月前
|
JavaScript 前端开发 开发者
SAP UI5 应用的 Component.js 文件是如何在运行时被加载的?
SAP UI5 应用的 Component.js 文件是如何在运行时被加载的?
30 0
|
5月前
|
缓存 JavaScript 应用服务中间件
Nginx+Tomcat代理环境下JS无法完全加载问题
Nginx+Tomcat代理环境下JS无法完全加载问题
|
18天前
|
JavaScript 前端开发
EasyUi js 加载数据表格DataGrid
EasyUi js 加载数据表格DataGrid
|
22天前
|
JavaScript
理解DOM树的加载过程(js的问题)
理解DOM树的加载过程(js的问题)
10 0
|
1月前
|
JavaScript
js判断图片是否加载完成
js判断图片是否加载完成
24 0
|
2月前
|
缓存 JavaScript 前端开发
前端工程化:优化JS加载速度
在现代Web应用中,JavaScript已成为必不可少的一部分,但是随着业务复杂度的增加,JS文件的体积也越来越大,导致网页加载速度变慢,影响用户体验。本文将介绍前端工程化的优化策略,以提高JS文件的加载速度。
19 2
|
3月前
|
移动开发 JavaScript 前端开发
分享36个JS滚动,29个JS进度条,12个JS日历代码,总有一款适合您
分享36个JS滚动,29个JS进度条,12个JS日历代码,总有一款适合您
26 0
|
3月前
|
移动开发 JavaScript 前端开发
分享21个JS抽奖转盘特效,36个JS表单验证,31个JS进度条,总有一款适合您
分享21个JS抽奖转盘特效,36个JS表单验证,31个JS进度条,总有一款适合您
24 0