我不知道,不過實現一個不難,因爲<script />有onload和onreadystatechange。還有就是,我們有Atlas。
Atlas中有個類:Sys.ScriptLoader,它的作用就是在頁面中依次地加載多個Script文件。在實現之前,先來分析一下這個類的代碼。
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}
112Sys.ScriptLoader.registerClass('Sys.ScriptLoader');
可以看出,Sys.ScriptLoader加載script的方法就是通過代碼依次向<header />裏添加<script />元素。事實上,它在Atlas中被使用的非常少。
事實上,Sys.ScriptLoader的代碼非常簡單,我添加的註釋越看越像畫蛇添足。值得注意的是所有的資源都被儘可能的釋放。尤其注意從第99行開始的代碼,if體內首先用臨時變量保留兩個全局變量,然後再將全局變量釋放。其目的就是避免在completionCallback在執行時拋出異常而導致的內存泄露,即使只有萬分之一的可能性。Javascript越多,則越容易造成內存泄露,在編寫JS代碼時最好注意這方面的問題。
接着解釋一下load方法的第一個參數references,原本以爲這一個Sys.Reference類的數組,結果發現其實相差甚遠。不管怎麼樣順便看一下該類的代碼。
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}
32Sys.Reference.registerSealedClass('Sys.Reference', null, Sys.ITypeDescriptorProvider, Sys.IDisposable);
33Sys.TypeDescriptor.addType('script', 'reference', Sys.Reference);
關心一下Sys.ScriptLoader類的代碼可知,reference數組的每個元素其實只是簡單的“{ url : "http://www.sample.com/sample.js", onscriptload : "alert(1)"}”形式的對象。不過這樣也好,想構造這麼一個數組也能輕易地使用JSON了。
到這裏,我想大家也應該想到了如何使用Sys.ScriptLoader輕而易舉地製作JS加載的進度條。不過既然寫到了這裏,也就繼續把它進行一個簡單的實現。
首先是aspx文件。
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數組的格式如下:
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:
2
3Jeffz.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這個如此清晰明瞭的範例,自己重新寫一個對於廣大開發人員來說,難道還不是易如反掌的事情嗎?