今天有人問我,163郵箱那樣的Javascript載入進度條是如何實現的。
我不知道,但實現一個不難,因為<script />有onload和onreadystatechange。還有就是,我們有Atlas。
Atlas中有個類別:Sys.ScriptLoader,它的作用就是在頁面中依序載入多個Script檔案。在實作之前,先來分析一下這個類別的程式碼。
1Sys.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}
112Sys.ScriptLoader.registerClass('Sys.ScriptLoader');
可以看出,Sys.ScriptLoader載入script的方法就是透過程式碼依序在<header />裡加入<script />元素。事實上,它在Atlas中被使用的非常少。
事實上,Sys.ScriptLoader的程式碼非常簡單,我加入的註解越看越像畫蛇添足。值得注意的是所有的資源都被盡可能的釋放。特別注意從第99行開始的程式碼,if體內先用臨時變數保留兩個全域變量,然後再釋放全域變數。其目的是避免在completionCallback在執行時拋出異常而導致的內存洩露,即使只有萬分之一的可能性。 Javascript越多,越容易造成記憶體洩露,在編寫JS程式碼時最好注意這方面的問題。
接著解釋一下load方法的第一個參數references,原本以為這一個Sys.Reference類別的數組,結果發現其實相差甚遠。不管怎麼樣順便看一下該類別的程式碼。
1Sys.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}
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檔。
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數組的格式如下:
1var 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:
1Type.registerNamespace('Jeffz.Sample');
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一樣建一個隊列嘛),但是程式碼量自然而然就上去了,開發的複雜度也提高了。
不過我認為,這種「單線程」的script載入已經足夠用於大多數情況了。而且如果真的有「特殊」要求,參考Sys.ScriptLoader這個如此清晰明了的範例,自己重新寫一個對於廣大開發人員來說,難道還不是易如反掌的事情嗎?
http://www.cnblogs.com/JeffreyZhao/archive/2006/09/13/502357.html