英文:blog.carbonfive.com
譯者:伯樂線上 – cucr
網址:http://web.jobbole.com/83360/
本文講什麼?
伴隨著JavaScript這種web瀏覽器指令碼語言的普及,對它的事件驅動互動模型,以及它與Ruby、Python和Java中常見的請求-響應模型的區別有一個基本瞭解,對您是有益的。在這篇文章中,我將解釋一些JavaScript併發模型的核心概念,包括其事件迴圈和訊息佇列,希望能夠提升你對一種語言的理解,這種語言你可能已經在使用但也許並不完全理解。
這篇文章是寫給誰的?
這篇文章是針對在客戶端或伺服器端使用或計劃使用JavaScript的web開發人員的。如果你已經精通事件迴圈,那麼這篇文章的大部分對你來說會很熟悉。對於那些還不是很精通的人,我希望能給你提供一個基本的瞭解,這樣可以更好地幫助你閱讀和編寫日常程式碼。
非阻塞I / O
在JavaScript中,幾乎所有的I/O都是非阻塞的。這包括HTTP請求,資料庫操作和磁碟讀寫,單執行緒執行要求在執行期執行一個操作時,提供一個回呼函式,然後繼續做其它的事情。當操作已經完成時,訊息和已提供的回呼函式一起插入到佇列。在將來的某個時候,訊息從佇列移除,回呼函式觸發。
雖然這種互動模型可能對已經習慣使用使用者介面的開發人員很熟悉,比如“mousedown,”和“click”事件在某一時刻被觸發。這與通常在伺服器端應用程式進行的同步式請求-響應模型是不同的。
讓我們來比較一下兩小塊程式碼,發出HTTP請求到www.google.com和輸出響應到控制檯。首先看看Ruby,配合使用Faraday(一個Ruby 的HTTP 客戶端開發庫):
response = Faraday.get ‘http://www.google.com’
puts response
puts ‘Done!’
執行路徑很容易跟蹤:
-
執行get方法,執行的執行緒等待,直到收到響應
-
從谷歌收到響應並傳回給呼叫者,它儲存在一個變數中
-
變數的值(在本例中,就是我們的響應)輸出到控制檯
-
值“Done!“輸出到控制檯
讓我們使用Node.js和Request庫在JavaScript做同樣的事情:
request(‘http://www.google.com’, function(error, response, body) {
console.log(body);
});
console.log(‘Done!’);
錶面上看略有不同,實際行為截然不同:
-
執行請求函式,傳遞一個匿名函式作為回呼,當響應在將來某個時候可用時執行回呼。
-
“Done!“立即輸出到控制檯
-
在將來的某個時候,響應傳回和回呼執行時,輸出它的內容到控制檯
事件迴圈
將呼叫者和響應解耦,使得JavaScript在執行期等待非同步操作完成和回呼觸發時可以做其他事情。但是這些回呼在記憶體中是如何組織的,按什麼順序執行?什麼導致他們被呼叫?
JavaScript執行時包含一個訊息佇列,它儲存了需要處理的訊息的串列和相關的回呼函式。這些訊息是以佇列的形式來響應回呼函式所涉及的外部事件(如滑鼠單擊或收到HTTP請求的響應)的。例如,如果使用者單擊一個按鈕,但沒有提供回呼函式,那麼也沒有訊息會被加入佇列。
在一次迴圈,佇列提取下一條訊息(每次提取稱為一次“tick”),當事件發生,該訊息的回呼執行。
回呼函式的呼叫在呼叫棧作為初始化frame(片段),由於JavaScript是單執行緒的,未來的訊息提取和處理因為等待棧的所有呼叫傳回而被停止。後續(同步)函式呼叫會新增新的呼叫frame到棧(例如,函式init呼叫函式changeColor)。
function init() {
var link = document.getElementById(“foo”);
link.addEventListener(“click”, function changeColor() {
this.style.color = “burlywood”;
});
}
init();
在這個例子中,當使用者單擊“foo”元素時,一條訊息(及其回呼函式changeColor)會被插入到佇列,並觸發“onclick“事件。當訊息離開佇列時,其回呼函式changeColor被呼叫。當changeColor傳回(或者是丟擲一個錯誤),事件迴圈仍在繼續。只要函式changeColor存在,並指定為“foo”元素的onclick方法的回呼,那麼在該元素上單擊會導致更多的訊息(和相關的回呼changeColor)插入佇列。
佇列附加訊息
如果一個函式在程式碼中按非同步呼叫(比如setTimeout),提供的回呼將最終作為一個不同的訊息佇列的一部分被執行,它將發生在事件迴圈的某個未來的動作上。例如:
function f() {
console.log(“foo”);
setTimeout(g, 0);
console.log(“baz”);
h();
}
function g() {
console.log(“bar”);
}
function h() {
console.log(“blix”);
}
f();
由於setTimeout的非阻塞特性,它的回呼將在至少0毫秒後觸發,而不是作為訊息的一部分被處理。在這個示例中,setTimeout被呼叫, 傳入了一個回呼函式g且延時0毫秒後執行。當我們指定時間到達(當前情況是,幾乎立即執行),一個單獨的訊息將被加入佇列(g作為回呼函式)。控制檯列印的結果會是像這樣:“foo”,“baz”,“blix”,然後是事件迴圈的下一個動作:“bar”。如果在同一個呼叫片段中,兩個呼叫都設定為setTimeout -傳遞給第二個引數的值也相同-則它們的回呼將按照呼叫順序插入佇列。
Web Workers
使用Web Workers允許您能夠將一項費時的操作在一個單獨的執行緒中執行,從而可以釋放主執行緒去做別的事情。worker(工作執行緒)包括一個獨立的訊息佇列,事件循 環,記憶體空間獨立於實體化它的原始執行緒。worker和主執行緒之間的通訊透過訊息傳遞,看起來很像我們往常常見的傳統事件程式碼示例。
首先,我們的worker:
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
然後,主要的程式碼塊在我們的HTML中以script-標簽存在:
// our main code, in a