事件迴圈

JavaScript 具有基於 **事件迴圈** 的執行時模型,它負責執行程式碼、收集和處理事件以及執行排隊的子任務。該模型與 C 和 Java 等其他語言中的模型截然不同。

執行時概念

以下部分解釋了一個理論模型。現代 JavaScript 引擎實現了並對描述的語義進行了大量最佳化。

視覺表示

A diagram showing how stacks are comprised of frames, heaps are comprised of objects, and queues are comprised of messages.

堆疊

函式呼叫形成一個 **幀** 堆疊。

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

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

const baz = bar(7); // assigns 42 to baz

操作順序

  1. 呼叫 bar 時,將建立一個包含對 bar 的引數和區域性變數的引用的第一個幀。
  2. bar 呼叫 foo 時,將建立一個第二個幀並將其推送到第一個幀的頂部,其中包含對 foo 的引數和區域性變數的引用。
  3. foo 返回時,將從堆疊中彈出頂部的幀元素(只剩下 bar 的呼叫幀)。
  4. bar 返回時,堆疊為空。

請注意,引數和區域性變數可能仍然存在,因為它們儲存在堆疊之外 - 因此它們可以被任何 巢狀函式 在其外部函式返回後很長時間內訪問。

物件在堆中分配,堆只是一個表示大塊(大部分非結構化)記憶體區域的名稱。

佇列

JavaScript 執行時使用訊息佇列,訊息佇列是待處理訊息的列表。每個訊息都關聯著一個用於處理該訊息的函式。

在 **事件迴圈** 過程中的某個時刻,執行時開始處理佇列中的訊息,從最舊的訊息開始。為此,將訊息從佇列中刪除,並使用該訊息作為輸入引數呼叫其對應的函式。與往常一樣,呼叫函式會為該函式使用建立一個新的堆疊幀。

函式處理持續進行,直到堆疊再次為空。然後,事件迴圈將處理佇列中的下一條訊息(如果有)。

事件迴圈

**事件迴圈** 由於其實現方式通常類似於以下內容而得名

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

queue.waitForMessage() 非同步等待訊息到達(如果訊息尚未到達且正在等待處理)。

"執行至完成"

每個訊息在任何其他訊息處理之前都完全處理。

這在推理程式時提供了一些很好的屬性,包括以下事實:只要函式執行,它就不會被搶佔,並且將在任何其他程式碼執行之前完全執行(並且可以修改函式操作的資料)。這與 C 不同,例如,如果函式在某個執行緒中執行,它可能在執行時系統執行另一個執行緒中的某些程式碼時被停止。

此模型的一個缺點是,如果訊息處理時間過長,則 Web 應用程式將無法處理使用者互動,如點選或滾動。瀏覽器透過 "指令碼執行時間過長" 對話方塊來緩解這個問題。一個要遵循的良好做法是使訊息處理簡短,如果可能的話,將一條訊息分成多條訊息。

新增訊息

在 Web 瀏覽器中,訊息通常在發生事件並且事件監聽器附加到該事件時新增。如果沒有監聽器,則事件將丟失。因此,點選具有點選事件處理程式的元素將新增一條訊息 - 同樣適用於任何其他事件。但是,有些事件會同步發生而無需訊息 - 例如,透過 click 方法模擬點選。

函式 setTimeout 的前兩個引數是待新增到佇列的訊息和時間值(可選;預設為 0)。時間值 表示將訊息推送到佇列的(最小)延遲。如果沒有其他訊息在佇列中,並且堆疊為空,則訊息將在延遲後立即處理。但是,如果存在訊息,則 setTimeout 訊息將不得不等待其他訊息被處理。因此,第二個引數表示最小時間 - 而不是保證時間。

以下示例演示了此概念(setTimeout 不會在計時器過期後立即執行)

js
const seconds = new Date().getTime() / 1000;

setTimeout(() => {
  // prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
  console.log(`Ran after ${new Date().getTime() / 1000 - seconds} seconds`);
}, 500);

while (true) {
  if (new Date().getTime() / 1000 - seconds >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

零延遲

零延遲並不意味著回撥將在零毫秒後觸發。使用 0(零)毫秒的延遲呼叫 setTimeout 不會在給定間隔後執行回撥函式。

執行取決於佇列中等待的任務數量。在下面的示例中,訊息 "this is just a message" 將在回撥中的訊息被處理之前寫入控制檯,因為延遲是執行時處理請求所需的最小時間(而不是保證時間)。

setTimeout 需要等待所有排隊訊息的程式碼完成,即使你為 setTimeout 指定了特定的時間限制。

js
(() => {
  console.log("this is the start");

  setTimeout(() => {
    console.log("Callback 1: this is a msg from call back");
  }); // has a default time value of 0

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

  setTimeout(() => {
    console.log("Callback 2: this is a msg from call back");
  }, 0);

  console.log("this is the end");
})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

多個執行時相互通訊

Web 工作執行緒或跨域 iframe 具有自己的堆疊、堆和訊息佇列。兩個不同的執行時只能透過使用 postMessage 方法傳送訊息來通訊。該方法會將訊息新增到另一個執行時,如果後者監聽 message 事件。

永不阻塞

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

存在像 alert 或同步 XHR 這樣的舊異常,但建議避免使用它們。注意:異常中存在異常(但通常是實現錯誤,而不是其他原因)。

另請參閱