MDN之JavaScript-高階(二)【Concurrency model and Event Loop併發模型與事件迴圈】

風靈使發表於2018-06-04
Web 開發技術> JavaScript >併發模型與事件迴圈

JavaScript 的併發模型基於"事件迴圈"。這個模型與像 C 或者 Java 這種其它語言中的模型截然不同。

執行時概念

下面的內容解釋了一個理論上的模型。現代 JavaScript 引擎著重實現和優化了描述的幾個語義。

視覺化描述

這裡寫圖片描述
執行棧中的程式碼(同步任務),總是在讀取"任務佇列"(非同步任務)之前執行。

函式呼叫形成了一個棧幀。

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));

當呼叫bar時,建立了第一個幀 ,幀中包含了bar的引數和區域性變數。當bar呼叫foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了foo的引數和區域性變數。當foo返回時,最上層的幀就被彈出棧(剩下bar函式的呼叫幀 )。當bar返回的時候,棧就空了。

物件被分配在一個堆中,即用以表示一個大部分非結構化的記憶體區域。

佇列

一個 JavaScript 執行時包含了一個待處理的訊息佇列。每一個訊息都有一個為了處理這個訊息相關聯的函式。

在 事件迴圈 時,runtime (執行時)總是從最先進入佇列的一個訊息開始處理佇列中的訊息。正因如此,這個訊息就會被移出佇列,並將其作為輸入引數呼叫與之關聯的函式。為了使用這個函式,呼叫一個函式總是會為其創造一個新的棧幀( stack frame),一如既往。

函式的處理會一直進行直到執行棧再次為空;然後事件迴圈(event loop)將會處理佇列中的下一個訊息(如果還有的話)。

事件迴圈

之所以稱為事件迴圈,是因為它經常被用於類似如下的方式來實現:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果當前沒有任何訊息queue.waitForMessage 會等待同步訊息到達。

"執行至完成"

每一個訊息完整的執行後,其它訊息才會被執行。當你分析你的程式時,這點提供了一些優秀的特性,包括每當一個函式執行時,它就不能被搶佔,並且在其他程式碼執行之前完全執行(且可以修改此函式操作的資料)。這與C語言不同,例如,如果函式線上程中執行,則可以在任何位置終止然後在另一個執行緒中執行其他程式碼。

這個模型的一個缺點在於當一個訊息需要太長時間才能完成,Web應用無法處理使用者的互動,例如點選或滾動。瀏覽器用“程式需要過長時間執行”的對話方塊來緩解這個問題。一個很好的做法是使訊息處理縮短,如果可能,將一個訊息裁剪成幾個訊息。

新增訊息

在瀏覽器裡,當一個事件出現且有一個事件監聽器被繫結時,訊息會被隨時新增。如果沒有事件監聽器,事件會丟失。所以點選一個附帶點選事件處理函式的元素會新增一個訊息。其它事件亦然。

呼叫 setTimeout 函式會在一個時間段過去後在佇列中新增一個訊息。這個時間段作為函式的第二個引數被傳入。如果佇列中沒有其它訊息,訊息會被馬上處理。但是,如果有其它訊息,setTimeout訊息必須等待其它訊息處理完。因此第二個引數僅僅表示最少的時間 而非確切的時間。

零延遲

零延遲並不是意味著回撥會立即執行。在零延遲呼叫 setTimeout 時,其並不是過了給定的時間間隔後就馬上執行回撥函式。其等待的時間基於佇列里正在等待的訊息數量。在下面的例子中,"this is just a message" 將會在回撥 (callback) 獲得處理之前輸出到控制檯,這是因為延遲是要求執行時 (runtime) 處理請求所需的最小時間,但不是有所保證的時間。

(function() {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from call back');
  });

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from call back1');
  }, 0);

  console.log('this is the end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// note that function return, which is undefined, happens here 
// "this is a msg from call back"
// "this is a msg from call back1"

多個執行時互相通訊

一個 web worker 或者一個跨域的iframe都有自己的棧,堆和訊息佇列。兩個不同的執行時只能通過 postMessage方法進行通訊。如果後者偵聽到message事件,則此方法會向其他執行時新增訊息。

永不阻塞

事件迴圈模型的一個非常有趣的特性是 JavaScript,與許多其他語言不同,它永不阻塞。 處理 I/O 通常通過事件和回撥來執行,所以當一個應用正等待IndexedDB查詢返回或者一個 XHR 請求返回時,它仍然可以處理其它事情,如使用者輸入。

例外是存在的,如 alert或者同步 XHR,但應該儘量避免使用它們。注意,例外的例外也是存在的(但通常是實現錯誤而非其它原因)。

相關文章