註:以下內容基於IE中GIF的onload事件的基礎上,故所有測試IE only
需要用到的幾個圖片
先看一個簡單的事實:
複製代碼 代碼如下:<SCRIPT LANGUAGE="JavaScript">
var img=new Image();
img.src="attachment/1178365293_0.gif";
img.onload=function()
{
alert("如要關閉請按住ESC鍵不放,並點擊關閉按鈕");
}
</SCRIPT>
如果不出所料的話你的IE應該彈出一大堆alert提示視窗了。注意是“一大堆”!
原因很簡單:IE對多幀GIF的onload事件重複執行,即每播放完一次動畫,就重新執行一次onload事件。
(註:按下ESC鍵會停止gif動畫的播放,故也會停止onload事件執行)
利用這一特性我們可以類比多線程的實現:
看下面的代碼:
複製代碼 代碼如下:image A onload執行次數:<span id="ThreadA">0</span><br>
image B onload執行次數:<span id="ThreadB">0</span><br>
image C onload執行次數:<span id="ThreadC">0</span>
<script>
function Img(threadID,src)
{
var img = new Image;
img.onload = function()
{
var c = parseInt(document.getElementById(threadID).innerHTML);
document.getElementById(threadID).innerHTML=isNaN(c)?1:++c;
}
img.src = src;
return img;
}
var imgA = new Img("ThreadA","attachment/1178365293_0.gif");
var imgB = new Img("ThreadB","attachment/1178365293_1.gif");
var imgC = new Img("ThreadC","attachment/1178365293_2.gif");
</script>
是不是有點意思了?
再看下面的代碼:
複製代碼 代碼如下:<script>
//by Rimifon
var Threads = new Array;
onload = function()
{
for(var C=1;C<501;C++)
{
Threads.push(new Thread(C));
}
}
function Go(sender)
{
var IsStart = sender.value=="全部開始";
for(var x in Threads)
{
Threads[x].Start = IsStart;
}
sender.value = "全部" + (IsStart?"暫停":"開始");
}
function Thread(ID)
{
this.Start = 0;
var cursor = this;
var span = document.createElement("span");
var counter = document.createTextNode("0");
span.appendChild(counter);
var div = document.createElement("div");
div.appendChild(document.createTextNode("線程" + ID + ":"));
div.style.cursor = "pointer";
div.onclick = function()
{
cursor.Start = !cursor.Start;
}
div.oncontextmenu = function()
{
img.onload = null;
this.removeNode(true);
return false;
}
div.appendChild(span);
document.body.appendChild(div);
var img = new Image;
img.onload = function()
{
if(cursor.Start) counter.data = parseInt(counter.data) + 1;
div.style.backgroundColor = cursor.Start?"#abcdef":"yellow";
}
img.src = "images/51js.gif";
}
</script>
<input type=button value="全部開始" onclick="Go(this)">
<input type=button value="彈出對話方塊" onclick="alert('對話方塊測試')">
部分代碼引自Rimifon。(轉載 http://Dnew.cn)
JavaScript多線程編程簡介
雖然有越來越多的網站在應用AJAX技術進行開發,但是構建一個複雜的AJAX應用仍然是一個難題。造成這些困難的主要原因是什麼呢?是與伺服器的非同步通訊問題?還是GUI程式設計問題呢?通常這兩項工作都是由傳統型程式來完成的,那究竟為何開發一個可以實現同樣功能的AJAX應用就這麼困難呢?
AJAX 開發中的難題
讓我們通過一個簡單的例子來認識這個問題。假設你要建立一個樹形結構的公告欄系統(BBS),它可以根據使用者請求與伺服器進行互動,動態載入每篇文章的資訊,而不是一次性從伺服器載入所有文章資訊。每篇文章有四個相關屬性:系統中可以作為唯一標識的ID、發貼人姓名、文章內容以及包含其所有子文章ID的數組資訊。首先假定有一個名為getArticle()的函數可以載入一篇文章資訊。該函數接收的參數是要載入文章的ID,通過它可從伺服器擷取文章資訊。它返回的對象包含與文章相關的四條屬性:id,name,content和children。常式如下:
複製代碼 代碼如下:function ( id ) {
var a = getArticle(id);
document.writeln(a.name + "" + a.content);
}
然而你也許會注意到,重複用同一個文章ID調用此函數,需要與伺服器之間進行反覆且無益的通訊。想要解決這個問題,可以考慮使用函數 getArticleWithCache(),它相當於一個帶有緩衝能力的getArticle()。在這個例子中,getArticle()返回的資料只是作為一個全域變數被儲存下來: 複製代碼 代碼如下:var cache = {};
function getArticleWithCache ( id ) {
if ( !cache[id] ) {
cache[id] = getArticle(id);
}
return cache[id];
}
現在已將讀入的文章緩衝起來,讓我們再來考慮一下函數backgroundLoad(),它應用我們上面提到的緩衝機制載入所有文章資訊。其用途是,當讀者在閱讀某篇文章時,從後台預先載入它所有子文章。因為文章資料是樹狀結構的,所以很容易寫一個遞迴的演算法來遍曆樹並且載入所有的文章: 複製代碼 代碼如下:function backgroundLoad ( ids ) {
for ( var i=0; i < ids.length; i++ ) {
var a = getArticleWithCache(ids[i]);
backgroundLoad(a.children);
}
}
backgroundLoad ()函數接收一個ID數組作為參數,然後通過每個ID調用前面定義過的getArticldWithCache()方法,這樣就把每個ID對應的文章緩衝起來。之後再通過已載入文章的子文章ID數組遞迴調用backgroundLoad()方法,如此整個文章樹就被緩衝起來。
到目前為止,一切似乎看起來都很完美。然而,只要你有過開發AJAX應用的經驗,你就應該知曉這種幼稚的實現方法根本不會成功,這個例子成立的基礎是預設 getArticle()用的是同步通訊。可是,作為一條基本原則,JavaScript要求在與伺服器進行互動時要用非同步通訊,因為它是單線程的。就簡單性而言,把每一件事情(包括GUI事件和渲染)都放在一個線程裡來處理是一個很好的程式模型,因為這樣就無需再考慮線程同步這些複雜問題。另一方面,他也暴露了應用開發中的一個嚴重問題,單線程環境看起來對使用者請求響應迅速,但是當線程忙於處理其它事情時(比如說調用getArticle()),就不能對使用者的滑鼠點擊和鍵盤操作做出響應。
如果在這個單線程環境裡進行同步通訊會發生什麼事情呢?同步通訊會中斷瀏覽器的執行直至獲得通訊結果。在等待通訊結果的過程中,由於伺服器的調用還沒有完成,線程會停止回應使用者並保持鎖定狀態直到調用返回。因為這個原因,當瀏覽器在等待伺服器響應時它不能對使用者行為作出響應,所以看起來像是凍結了。當執行 getArticleWithCache()和backgroundLoad()會有同樣的問題,因為它們都是基於getArticle()函數的。由於下載所有的文章可能會耗費很可觀的一段時間,因此對於backgroundLoad()函數來說,瀏覽器在此段時間內的凍結就是一個很嚴重的問題——既然瀏覽器都已經凍結,當使用者正在閱讀文章時就不可能首先去執行後台預先載入資料,如果這樣做連當前的文章都沒辦法讀。
如上所述,既然同步通訊在使用中會造成如此嚴重的問題,JavaScript就把非同步通訊作為一條基本原則。因此,我們可以基於非同步通訊改寫上面的程式。 JavaScript要求以一種事件驅動的程式設計方式來寫非同步通訊程式。在很多場合中,你都必須指定一個回調程式,一旦收到通訊響應,這個函數就會被調用。例如,上面定義的getArticleWithCache()可以寫成這樣: 複製代碼 代碼如下:var cache = {};
function getArticleWithCache ( id, callback ) {
if ( !cache[id] ) {
callback(cache[id]);
} else {
getArticle(id, function( a ){
cache[id] = a;
callback(a);
});
}
}
這個程式也在內部調用了getArticle()函數。然而需要注意的是,為非同步通訊設計的這版getArticle()函數要接收一個函數作為第二個參數。當調用這個getArticle()函數時,與從前一樣要給伺服器發送一個請求,不同的是,現在函數會迅速返回而非等待伺服器的響應。這意味著,當執行權交回給調用程式時,還沒有得到伺服器的響應。如此一來,線程就可以去執行其它任務直至獲得伺服器響應,並在此時調用回呼函數。一旦得到伺服器響應, getArticle()的第二個參數作為預先定義的回呼函數就要被調用,伺服器的傳回值即為其參數。同樣的,getArticleWithCache ()也要做些改變,定義一個回調參數作為其第二個參數。這個回呼函數將在被傳給getArticle()的回呼函數中調用,因而它可以在伺服器通訊結束後被執行。
單是上面這些改動你可能已經認為相當複雜了,但是對backgroundLoad()函數做得改動將會更複雜,它也要被改寫成可以處理回呼函數的形式: 複製代碼 代碼如下:function backgroundLoad ( ids, callback ) {
var i = 0;
function l ( ) {
if ( i < ids.length ) {
getArticleWithCache(ids[i++], function( a ){
backgroundLoad(a.children, l);
});
} else {
callback();
}
}
l();
}
改動後的backgroundLoad()函數看上去和我們以前的那個函數已經相去甚遠,不過他們所實現的功能並無二致。這意味著這兩個函數都接受ID數組作為參數,對於數組裡的每個元素都要調用getArticleWithCache(),再應用已經獲得子文章ID遞迴調用backgroundLoad ()。不過同樣是對數組的逐一查看,新函數中的就不太好辨認了,以前的程式中是用一個for迴圈陳述式完成的。為什麼實現同樣功能的兩套函數是如此的大相徑庭呢?
這個差異源於一個事實:任何函數在遇到有需要同伺服器進行通訊情況後,都必須立刻返回,例如getArticleWithCache()。除非原來的函數不在執行當中,否則應當接受伺服器響應的回呼函數都不能被調用。對於JavaScript,在迴圈過程中中斷程式並在稍後從這個斷點繼續開始執行程式是不可能的,例如一個for語句。因此,本例利用遞迴傳遞迴調函數實現迴圈結構而非一個傳統迴圈語句。對那些熟悉連續傳送風格(CPS)的人來說,這就是一個 CPS的手動實現,因為不能使用迴圈文法,所以即便如前面提到的遍曆樹那麼簡單的程式也得寫得很複雜。與事件驅動程式設計相關的問題是控制流程問題:迴圈和其它控制流程運算式可能比較難理解。
這裡還有另外一個問題:如果你把一個沒有應用非同步通訊的函數轉換為一個使用非同步通訊的函數,那麼重寫的函數將需要一個回呼函數作為新增參數,這為已經存在的APIs造成了很大問題,因為內在的改變沒有把影響限於內部,而是導致整體混亂的APIs以及API的其它使用者的改變。
造成這些問題目的根本原因是什麼呢?沒錯,正是JavaScript單線程機制導致了這些問題。在單線程裡執行非同步通訊需要事件驅動程式設計和複雜的語句。如果當程式在等待伺服器的響應時,有另外一個線程可以來處理使用者請求,那麼上述複雜技術就不需要了。
試試多線程編程
讓我來介紹一下Concurrent.Thread,它是一個允許JavaScript進行多線程編程的庫,應用它可以大大緩解上文提及的在AJAX開發中與非同步通訊相關的困難。這是一個用JavaScript寫成的免費的軟體庫,使用它的前提是遵守Mozilla Public License和GNU General Public License這兩個協議。你可以從他們的網站 下載原始碼。
馬上來下載和使用源碼吧!假定你已經將下載的源碼儲存到一個名為Concurrent.Thread.js的檔案夾裡,在進行任何操作之前,先運行如下程式,這是一個很簡單的功能實現: 複製代碼 代碼如下:<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/javascript">
Concurrent.Thread.create(function(){
var i = 0;
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
});
</script>
執行這個程式將會順序顯示從0開始的數字,它們一個接一個出現,你可以滾屏來看它。現在讓我們來仔細研究一下代碼,他應用while(1)條件製造了一個不會中止的迴圈,通常情況下,象這樣不斷使用一個並且是唯一一個線程的JavaScript程式會導致瀏覽器看起來象凍結了一樣,自然也就不會允許你滾屏。那麼為什麼上面的這段程式允許你這麼做呢?關鍵之處在於while(1)上面的那條Concurrent.Thread.create()語句,這是這個庫提供的一個方法,它可以建立一個新線程。被當做參數傳入的函數在這個新線程裡執行,讓我們對程式做如下微調: 複製代碼 代碼如下:<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/javascript">
function f ( i ){
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
}
Concurrent.Thread.create(f, 0);
Concurrent.Thread.create(f, 100000);
</script>
在這個程式裡有個新函數f()可以重複顯示數字,它是在程式段起始定義的,接著以f()為參數調用了兩次create()方法,傳給create()方法的第二個參數將會不加修改地傳給f()。執行這個程式,先會看到一些從0開始的小數,接著是一些從100,000開始的大數,然後又是接著前面小數順序的數字。你可以觀察到程式在交替顯示小數和大數,這說明兩個線程在同時運行。
讓我來展示Concurrent.Thread的另外一個用法。上面的例子調用create()方法來建立新線程。不調用庫裡的任何APIs也有可能實現這個目的。例如,前面那個例子可以這樣寫: 複製代碼 代碼如下:<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var i = 1;
while ( 1 ) {
document.body.innerHTML += i++ + "<br>";
}
</script>
在script 標籤內,很簡單地用JavaScript寫了一個無窮迴圈。你應該注意到標籤內的type屬性,那裡是一個很陌生的值(text/x- script.multithreaded-js),如果這個屬性被放在script標籤內,那麼Concurrent.Thread就會在一個新的線程內執列標籤之間的程式。你應當記住一點,在本例一樣,必須將Concurrent.Thread庫包含進來。
有了Concurrent.Thread,就有可能自如的將執行環境線上程之間進行切換,即使你的程式很長、連續性很強。我們可以簡要地討論下如何執行這種操作。簡言之,需要進行代碼轉換。粗略地講,首先要把傳遞給create()的函數轉換成一個字串,接著改寫直至它可以被分批分次執行。然後這些程式可以依照發送器逐步執行。發送器負責協調多線程,換句話說,它可以在適當的時候做出調整以便每一個修改後的函數都會得到同等機會運行。 Concurrent.Thread實際上並沒有建立新的線程,僅僅是在原本單線程的基礎上類比了一個多線程環境。
雖然轉換後的函數看起來是運行在不同的線程內,但是實際上只有一個線程在做這所有的事情。在轉換後的函數內執行同步通訊仍然會造成瀏覽器凍結,你也許會認為以前的那些問題根本就沒有解決。不過你不必耽心,Concurrent.Thread提供了一個應用JavaScript 的非同步通訊方式實現的定製通訊庫,它被設計成當一個線程在等待伺服器的響應時允許其它線程運行。這個通訊庫存於 Concurrent.Thread.Http下。它的用法如下所示: 複製代碼 代碼如下:<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var req = Concurrent.Thread.Http.get(url, ["Accept", "*"]);
if (req.status == 200) {
alert(req.responseText);
} else {
alert(req.statusText);
}
</script>
get()方法,就像它的名字暗示的那樣,可以通過HTTP的GET方法獲得指定URL的內容,它將目標URL作為第一個參數,將一個代表HTTP要求標頭的數組作為可選的第二個參數。get()方法與伺服器互動,當得到伺服器的響應後就返回一個XMLHttpRequest對象作為傳回值。當get()方法返回時,已經收到了伺服器響應,所以就沒必要再用回呼函數接收結果。自然,也不必再耽心當程式等待伺服器的響應時瀏覽器凍結的情況了。另外,還有一個 post()方法可以用來發送資料到伺服器: 複製代碼 代碼如下:<script type="text/javascript" src="Concurrent.Thread.js"></script>
<script type="text/x-script.multithreaded-js">
var req = Concurrent.Thread.Http.post(url, "key1=val1&key2=val2");
alert(req.statusText);
</script>
post()方法將目的URL作為第一個參數,要發送的內容作為第二個參數。像get()方法那樣,你也可以將要求標頭作為可選的第三個參數。
如果你用這個通訊庫實現了第一個例子當中的getArticle()方法,那麼你很快就能應用文章開頭樣本的那種簡單的方法寫出getArticleWithCache(),backgroundLoad ()以及其它調用了getArticle()方法的函數了。即使是那版backgroundLoad()正在讀文章資料,照例還有另外一個線程可以對使用者請求做出響應,瀏覽器因此也不會凍結。現在,你能理解在JavaScript中應用多線程有多實用了?
想瞭解更多
我向你介紹了一個可以在JavaScript中應用多線程的庫:Concurrent.Thread。這篇文章的內容只是很初級的東西,如果你想更深入的瞭解,我推薦您去看the tutorial。它提供有關Concurrent.Thread用法的更多內容,並列出了可供進階使用者使用的文檔,是最適合起步的材料。訪問他們的網站也不錯,那裡提供更多資訊。
有關作者
Daisuke Maki:從International Christian大學文科學院自然科學分部畢業後(取得文學學士學位),又在Electro-Communications大學的研究生院資訊專業攻讀碩士學位。擅長Web開發和應用JavaScript的AJAX。他開發了Concurrent.Thread。2006財政年度在日本資訊技術促進機構(IPA)指導的項目Explatory Software Project中應用了這個設計。
目前已經擁有一個工學碩士學位的他正在Electro-Communications大學的研究生院註冊攻讀博士學位。