JavaScript的 Event Loop 模型
前言
現如今,作為瀏覽器腳本語言的JavaScript幾乎無處不在。作為軟件開發人員,接觸JavaScript語言也是不可避免的。由于項目需要,本人接觸這門語言也有將近一年了。寫這篇文章的原因也是對JavaScript語言本身的編程模型做一個總結,借以鞏固自己對JavaScript的理解,并希望舉一反三,通過了解其獨特的編程模型,進一步消化吸收,提高自己知識的深度與廣度。本文將介紹一些有關JavaScript并發模型的一些核心概念,包括event loop和消息隊列等…
非阻塞IO
關于阻塞與非阻塞,如果不了解的話可以看看本博客 Linux下的五種IO模型 這篇文章。
由于JavaScript是單線程執行的,若IO操作阻塞的話會導致程序完全阻塞住,如果在瀏覽器環境運行的話,將出現卡死現象,嚴重影響用戶體驗。所以,在JavaScript中,絕大多數IO操作都是非阻塞的,其中包括:HTTP請求,數據庫操作以及磁盤讀寫(Node.js)。當遇到IO操作時,程序只需要提供一個回調函數就可以繼續往下執行而不必等待IO操作完成。當IO操作完成后,完成消息將綁定對應的回調函數壓入消息隊列。在未來某個時刻(主線程以及消息隊列排在當前消息前面的回調執行完畢后),消息出隊,回調函數將觸發執行。
下面以一個http請求為例子,根據其輸出對兩種風格的模型進行對比:
conn = httplib.HTTPConnection("http://www.google.com")
conn.request(...)
response = conn.getresponse()
print respons
print "done!"
運行結果:
xxx
done!
流程很簡單:
- 請求方法執行,執行線程等待直到接收到響應
- 接收到來自Google的響應并返回
- 將返回值輸出到控制臺
- “done”輸出至控制臺
同一種功能的JavaScript實現如下:
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
運行結果:
done!
xxx
雖然寫法上差別不大,但是輸出卻大相徑庭。以下為程序具體執行流程:
- 請求函數執行,傳入匿名函數作為響應返回后的回調函數
- “done!”被立即輸出至控制臺
- 未來某個時刻,接收到來自Google的響應,回調函數被執行,輸出響應信息
Event Loop
由于在請求時不需要等待響應返回,程序可以在執行完請求函數后無需等待響應繼續執行其他邏輯,當異步請求完成后,再執行其回調函數就行了。不過這里需要明確幾個問題:回調函數寄存在哪?以什么順序進行執行?由什么觸發回調?
消息隊列(Message Queue)
JavaScript運行環境包含一個消息隊列,消息隊列存儲了一系列待處理的消息,這些消息與對應的回調函數綁定在一起。當綁定了回調函數的外部事件(例如鼠標點擊、接收到HTTP請求的響應等)被觸發,消息將進入消息隊列等待處理。但如果事件沒有綁定回調函數,消息是不會入隊的。
事件循環(Event Loop)
在一次循環中,消息隊列出隊一個消息,并執行對應的回調函數,每一次出隊我們稱作”tick”;舉一個例子:
function init_func() {
var func1 = function () {
var func2 = function () {
var func3 = function () {
var success = function() {
console.log("success!");
};
request('http://www.google.com', success);
console.log('Done!');
};
func3();
};
func2();
};
func1();
}
init_func();
首先,上面的程序定義了5個函數,分別為init_func、func1、2、3以及success。init_func作為調用棧的初始幀,由于JavaScript是單線程執行的,故只有在棧中的函數都返回后,消息隊列中消息綁定的回調函數才會執行。以上面的程序為例,當執行init_func時,在函數內部又調用了func1,此時func1入棧,func1內部又調用了func2,繼續入棧…以此類推,直到func3內部調用了請求Google并傳入了success回調。這一步完成后,func3、2、1、init_func的棧幀相繼彈出。可能在此期間,Google的響應被接收了,綁定了success回調函數的message入棧了。不過無論message何時入棧,都要等待調用棧中所有幀都被彈出后才會觸發一次”tick”,此時success回調才被執行(細節在下一節討論)。具體原理圖如下:
需要注意的是,與HTTP請求不同,用戶事件(比如click)是一直存在的,只要用戶有點擊動作,新的message就會不斷的入隊,而不像上面的例子一次性入隊。每當message出隊回調函數會不斷被執行。
回調函數的執行時機
如果代碼中調用了一個異步函數(比如下面的setTimeout),消息,會新生成一個消息入隊,在Event Loop未來的某次tick中出隊并調用與之綁定的回調函數。
function func() {
console.info("foo");
setTimeout(tick, 0);
console.info("baz");
func2();
}
function tick() {
console.info("bar");
}
function func2() {
console.info("blix");
}
func();
在這個例子中,setTimeout被調用,傳入了回調函數tick以及時間間隔0毫秒。經過指定時間后(幾乎是立即),一個與當前獨立的消息入隊,并綁定回調函數tick。調用棧中所有幀彈出后,執行隊列中消息出隊并執行所綁定的回調函數。具體輸出如下:
foo
baz
blix
baz
如果在func中連續調用多個setTimeout函數,則回調函數執行順序依賴setTimeout執行先后順序。
閉包
JavaScript對 閉包 的支持允許回調函數在執行時訪問其外部的上下文,上下文在聲明回調的函數彈出調用棧后仍然有效。考慮下面的例子:
function say_hello() {
var name = "programmer";
console.info("hello, decaywood!");
var say_hello_again = function() {
console.info("hello, " + name + "!")
};
setTimeout(say_hello_again, 1000);
}
say_hello();
在這個例子中,say_hello函數被執行時定義了變量name。之后setTimeout函數被執行,約1000毫秒后,綁定了say_hello_again回調的消息入隊。之后say_hello函數返回,棧幀彈出結束第一個消息的處理,但name變量仍然可以通過閉包被引用,而不是被垃圾回收。當第二個消息被處理(say_hello_again回調),它保持了對在外部函數上下文中聲明的name變量的訪問。一旦回調函數執行結束,header變量可以被垃圾回收。
執行結果:
hello, decaywood!
hello, programmer!
結語
JavaScript事件驅動的交互模型不同于許多程序員習慣的請求-響應模型,但如你所見,它并不復雜。使用簡單的消息隊列和事件循環,JavaScript使得開發人員在構建他們的系統時使用大量asynchronously-fired(異步-觸發)回調函數,讓運行時環境能在等待外部事件觸發的同時處理并發操作。然而,這不過是并發的一種方法而已。
來自: http://ourjs.com/detail/5775c41e88feaf2d031d2554