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

简介:
 今天有人问我,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 : " [url]http://www.sample.com/sample.js[/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,如需转载请自行联系原作者

相关文章
|
5月前
|
缓存 JavaScript 前端开发
高效打造跨平台桌面应用:Electron加载服务器端JS
【9月更文挑战第17天】Electron 是一个基于 Chromium 和 Node.js 的开源框架,允许使用 HTML、CSS 和 JavaScript 构建跨平台桌面应用。加载服务器端 JS 可增强应用灵活性,实现代码复用、动态更新及实时通信。通过 HTTP 请求、WebSocket 或文件系统可实现加载,但需注意安全性、性能和兼容性问题。开发者应根据需求选择合适方法并谨慎实施。
213 3
|
3月前
|
缓存 前端开发 JavaScript
JavaScript加载优化
JavaScript加载优化
|
3月前
|
缓存 前端开发 JavaScript
优化CSS和JavaScript加载
优化CSS和JavaScript加载
|
3月前
|
缓存 前端开发 JavaScript
优化CSS和JavaScript加载
Next.js和Nuxt.js在优化CSS和JavaScript加载方面提供了多种策略和工具。Next.js通过代码拆分、图片优化和特定的CSS/JavaScript优化措施提升性能;Nuxt.js则通过代码分割、懒加载、预渲染静态页面、Webpack配置和服务端缓存来实现优化。两者均能有效提高应用性能。
|
6月前
|
编解码 缓存 算法
Three.js如何降低3D模型的大小以便更快加载
为加快600MB的3D模型在Three.js中的加载速度,可采用多种压缩方法:1) 减少顶点数,使用简化工具或LOD技术;2) 压缩纹理,降低分辨率或转为KTX2等格式;3) 采用高效文件格式如glTF 2.0及draco压缩;4) 合并材质减少数量;5) 利用Three.js内置优化如BufferGeometry;6) 按需分批加载模型;7) Web Workers后台处理;8) 多模型合并减少绘制;9) 使用Texture Atlas及专业优化工具。示例代码展示了使用GLTFLoader加载优化后的模型。
691 12
|
6月前
|
前端开发 JavaScript Linux
【Azure 应用服务】在Azure App Service for Linux环境中,部署的Django应用,出现加载css、js等静态资源文件失败
【Azure 应用服务】在Azure App Service for Linux环境中,部署的Django应用,出现加载css、js等静态资源文件失败
|
6月前
|
JavaScript 前端开发
JavaScript引入全攻略:提升网页加载速度的秘诀!
JavaScript引入全攻略:提升网页加载速度的秘诀!
|
6月前
|
资源调度 JavaScript 前端开发
如何大幅减少 Vue.js 中的包大小和加载时间,提升用户体验!
如何大幅减少 Vue.js 中的包大小和加载时间,提升用户体验!
|
7月前
|
监控 JavaScript 前端开发
Node中的AsyncLocalStorage 使用问题之Node.js内部模块和外部模块的加载的问题如何解决
Node中的AsyncLocalStorage 使用问题之Node.js内部模块和外部模块的加载的问题如何解决
|
7月前
命令行加载特效 【cli-spinner.js】 实用教程
命令行加载特效 【cli-spinner.js】 实用教程
64 0

热门文章

最新文章