JavaScript的 Event Loop 模型

jiangkeu 8年前發布 | 12K 次閱讀 消息系統 JavaScript開發 JavaScript

前言

現如今,作為瀏覽器腳本語言的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!

流程很簡單:

  1. 請求方法執行,執行線程等待直到接收到響應
  2. 接收到來自Google的響應并返回
  3. 將返回值輸出到控制臺
  4. “done”輸出至控制臺

同一種功能的JavaScript實現如下:

request('http://www.google.com', function(error, response, body) {
  console.log(body);
});
console.log('Done!');

運行結果:

done!
xxx

雖然寫法上差別不大,但是輸出卻大相徑庭。以下為程序具體執行流程:

  1. 請求函數執行,傳入匿名函數作為響應返回后的回調函數
  2. “done!”被立即輸出至控制臺
  3. 未來某個時刻,接收到來自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

 

 本文由用戶 jiangkeu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!