您的位置:首页 > Web前端 > JavaScript

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

2006-09-13 01:23 615 查看
  今天有人问我,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 : "http://www.sample.com/sample.js", 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 : "http://www.sample.com/sample1.js", cost : costOfLoading1 },
4





{ url : "http://www.sample.com/sample2.js", cost : costOfLoading2 },
5





{ url : "http://www.sample.com/sample3.js", 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这个如此清晰明了的范例,自己重新写一个对于广大开发人员来说,难道还不是易如反掌的事情吗?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息