事件迴圈
JavaScript 具有基於 **事件迴圈** 的執行時模型,它負責執行程式碼、收集和處理事件以及執行排隊的子任務。該模型與 C 和 Java 等其他語言中的模型截然不同。
執行時概念
以下部分解釋了一個理論模型。現代 JavaScript 引擎實現了並對描述的語義進行了大量最佳化。
視覺表示
堆疊
函式呼叫形成一個 **幀** 堆疊。
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
操作順序
- 呼叫
bar時,將建立一個包含對bar的引數和區域性變數的引用的第一個幀。 - 當
bar呼叫foo時,將建立一個第二個幀並將其推送到第一個幀的頂部,其中包含對foo的引數和區域性變數的引用。 - 當
foo返回時,將從堆疊中彈出頂部的幀元素(只剩下bar的呼叫幀)。 - 當
bar返回時,堆疊為空。
請注意,引數和區域性變數可能仍然存在,因為它們儲存在堆疊之外 - 因此它們可以被任何 巢狀函式 在其外部函式返回後很長時間內訪問。
堆
物件在堆中分配,堆只是一個表示大塊(大部分非結構化)記憶體區域的名稱。
佇列
JavaScript 執行時使用訊息佇列,訊息佇列是待處理訊息的列表。每個訊息都關聯著一個用於處理該訊息的函式。
在 **事件迴圈** 過程中的某個時刻,執行時開始處理佇列中的訊息,從最舊的訊息開始。為此,將訊息從佇列中刪除,並使用該訊息作為輸入引數呼叫其對應的函式。與往常一樣,呼叫函式會為該函式使用建立一個新的堆疊幀。
函式處理持續進行,直到堆疊再次為空。然後,事件迴圈將處理佇列中的下一條訊息(如果有)。
事件迴圈
**事件迴圈** 由於其實現方式通常類似於以下內容而得名
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage() 非同步等待訊息到達(如果訊息尚未到達且正在等待處理)。
"執行至完成"
每個訊息在任何其他訊息處理之前都完全處理。
這在推理程式時提供了一些很好的屬性,包括以下事實:只要函式執行,它就不會被搶佔,並且將在任何其他程式碼執行之前完全執行(並且可以修改函式操作的資料)。這與 C 不同,例如,如果函式在某個執行緒中執行,它可能在執行時系統執行另一個執行緒中的某些程式碼時被停止。
此模型的一個缺點是,如果訊息處理時間過長,則 Web 應用程式將無法處理使用者互動,如點選或滾動。瀏覽器透過 "指令碼執行時間過長" 對話方塊來緩解這個問題。一個要遵循的良好做法是使訊息處理簡短,如果可能的話,將一條訊息分成多條訊息。
新增訊息
在 Web 瀏覽器中,訊息通常在發生事件並且事件監聽器附加到該事件時新增。如果沒有監聽器,則事件將丟失。因此,點選具有點選事件處理程式的元素將新增一條訊息 - 同樣適用於任何其他事件。但是,有些事件會同步發生而無需訊息 - 例如,透過 click 方法模擬點選。
函式 setTimeout 的前兩個引數是待新增到佇列的訊息和時間值(可選;預設為 0)。時間值 表示將訊息推送到佇列的(最小)延遲。如果沒有其他訊息在佇列中,並且堆疊為空,則訊息將在延遲後立即處理。但是,如果存在訊息,則 setTimeout 訊息將不得不等待其他訊息被處理。因此,第二個引數表示最小時間 - 而不是保證時間。
以下示例演示了此概念(setTimeout 不會在計時器過期後立即執行)
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 指定了特定的時間限制。
(() => {
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 事件。