Contents

什麼是事件循環以及它如何提高應用程序性能?

JavaScript 是一種單線程語言,在處理任務時按照先來先服務的原則運行。事件循環功能允許異步處理事件和回調,有效地模擬 JavaScript 環境中的多任務處理功能。因此,它可以確保使用該編程語言編寫的任何應用程序的最佳性能。

什麼是 JavaScript 事件循環?

JavaScript 的事件循環構成了每個 JavaScript 應用程序的一個組成部分,有助於以順序方式管理任務,同時允許主執行線程的順利進行。因此,這種現象通常被稱為“異步編程”。

事件循環維護要執行的操作列表,並按順序將其提交給相應的 Web API 進行處理。 JavaScript 監督此過程並根據其複雜性管理每個操作的執行。

要理解 JavaScript 事件循環和異步編程的必要性,必須掌握它所解決的根本問題。

以這段代碼為例:

 function longRunningFunction() {
  // This function does something that takes a long time to execute.
  for (var i = 0; i < 100000; i\+\+) {
    console.log("Hello")
  }
}

function shortRunningFunction(a) {
  return a * 2 ;
}

function main() {
  var startTime = Date.now();
  longRunningFunction();
  
  var endTime = Date.now();

  // Prints the amount of time it took to execute functions
  console.log(shortRunningFunction(2));
  console.log("Time taken: " \+ (endTime-startTime) \+ " milliseconds");
}

main();

本代碼引入了一個名為“longRunningFunction”的函數。所述功能被設計為執行複雜且耗時的操作。具體來說,它採用了一個迭代十萬次的“for”循環。因此,命令“console.log(“Hello”)”以相同的迭代次數執行,導致十萬個消息“Hello”實例被記錄到控制台。

程序的性能可能會受到計算機處理速度的影響,導致shortRunningFunction()的執行被延遲,直到前面的函數完成為止。

提供的信息比較了兩個函數的執行時間,如下所示:

/bc/images/longfunction_console.jpg

然後是單個 ShortRunningFunction():

/bc/images/shortfunction_console.jpg

當努力獲得最佳應用程序性能時,需要 2,351 毫秒才能完成的過程與根本不需要時間的過程之間的區別變得非常明顯。

事件循環如何幫助提高應用程序性能

事件循環包含一系列互連的階段和組件,共同促進系統的運行。

調用堆棧

JavaScript 調用堆棧在管理應用程序內的函數和事件調用方面發揮著至關重要的作用。在編譯過程中,JavaScript 代碼從上到下進行。相比之下,當 Node.js 處理代碼時,它從底部開始向上工作,在遇到函數定義時將其一一推送到調用堆棧上。

調用堆棧用於保留執行上下文並確保函數調用的正確順序,通過其實現為後進先出 (LIFO) 結構來實現這一點。

程序內函數的排列由堆棧控制,其中最近添加的項目保留在頂部,直到被刪除或執行。因此,在執行 JavaScript 代碼時,壓入調用堆棧的最終函數框架可能是第一個彈出並按順序執行的函數框架,從而保留所需的操作順序。

/bc/images/callstackdiagram.jpg

使用 JavaScript 會導致從堆棧中刪除每個幀,直到其內容耗盡,這意味著所有函數都已完成執行。

Libuv Web API

JavaScript 異步程序的核心是 libuv 。 libuv 庫是用 C 編程語言編寫的,可以與操作系統的低級 API 進行交互。該庫將提供多個 API,允許 JavaScript 代碼與其他代碼並行運行。用於創建線程的 API、用於線程間通信的 API 以及用於管理線程同步的 API。

在 Node.js 環境中使用 setTimeout 時,底層 libuv 庫負責監督事件循環並在預定義時間間隔到期時執行指定的回調函數。

類似地,在進行異步網絡操作時,libuv 採用非阻塞方法,通過允許其他進程在等待輸入/輸出 (I/O) 過程完成時不受阻礙地進行的方式來管理此類任務。

回調和事件隊列

回調和事件隊列充當預期執行的回調函數的等待區域。一旦由 libuv 啟動的異步任務完成,其各自的回調函數將被放置在該隊列中以等待進一步的指令。

順序如下:

利用 libuv 的功能,JavaScript 將異步操作卸載到 libuv 中以進行高效管理,從而可以毫無延遲地無縫進行後續任務。

JavaScript 中的異步操作完成後,相應的回調函數將添加到為此類任務指定的隊列中。

利用 JavaScript 可以按照預定順序執行各種任務,因為它會繼續執行後續操作,直到所有操作都按各自的順序完成。

一旦調用堆棧中的所有任務都已處理完畢並且沒有進一步的指令要執行,JavaScript 就會檢查回調隊列。回調隊列充當稍後需要執行或滿足某些條件時需要執行的函數的保存區域。通過檢查此隊列,JavaScript 可以確保在終止其操作之前處理所有待處理的任務。

如果隊列中存在任何回調,它將被推送到調用堆棧的頂部並相應地執行。

通過利用異步方法,主線程在執行非維護操作時保持不受阻礙,回調隊列保證相關回調函數在完成後按時間順序執行。

事件循環

/bc/images/eventloopdiagram.jpg

事件循環包含一個稱為微任務隊列的特定隊列,該隊列旨在容納在調用堆棧中當前活動任務完成後立即執行的微任務。這些任務在事件循環的任何後續渲染或迭代之前執行。微任務在事件循環中比標準任務擁有更高的優先級,並且優先於它們。

在編程中使用 Promise 時,一種常見的方法是創建微任務。在解決或拒絕 Promise 後,其各自的 **.then() 或.catch()** 回調將添加到微任務隊列中。該隊列可用於管理正在進行的操作之後需要立即關注的任務,包括對用戶界面組件的更新和對應用程序狀態的修改。

這種網絡應用程序的說明性實例需要用戶通過圖形用戶界面(GUI)與其交互,其中他們可以執行諸如單擊按鈕之類的動作以以非阻塞方式從服務器請求信息。此過程涉及系統在每次按下按鈕時執行異步任務,然後繼續遠程檢索數據而不停止其他操作。

在沒有微任務的情況下,可以通過考慮以下事件序列來理解此特定任務的事件循環的操作:

⭐用戶重複點擊按鈕。

單擊按鈕後,將啟動非阻塞數據檢索操作。

JavaScript 中的數據獲取操作完成後,相關的回調函數將被添加到標準任務隊列中進行處理。

事件循環的啟動涉及處理標準任務隊列中的任務,其特徵在於其普通和習慣的操作方式。

用戶界面更新的執行取決於數據檢索過程的結果,一旦常規任務授予其啟動許可,就會發生用戶界面更新。

微任務在事件循環內的獨特機制下運行。宏任務遵循的標準方法不適用於它們。

單擊指定按鈕後,用戶以異步方式發起數據請求,從而開始一系列操作。

成功檢索數據後,事件循環將相應的回調函數放入微任務隊列中以供執行。

事件循環旨在根據任務各自的類型(例如用戶界面任務和微任務)確定優先級,從而有效地處理任務。一旦任務完成,例如單擊按鈕,事件循環將自動繼續處理隊列中任何待處理的微任務,而不會延遲。

響應於數據檢索結果執行用戶界面更新通過先於後續例行操作確保了及時且反應性的用戶體驗。

這是一個代碼示例:

 const fetchData = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('Data from fetch'), 2000);
  });
};

document.getElementById('fetch-button').addEventListener('click', () => {
  fetchData().then(data => {//This UI update will run before the next rendering cycle
    updateUI(data);
  });
});

在本例中單擊“Fetch”按鈕後,將調用函數“fetchData()”。檢索數據後,該過程被安排為微任務。數據採集​​任務完成後,用戶界面更新會立即發生,先於任何其他渲染或事件循環活動。

為了防止事件循環中其他正在進行的任務導致延遲,這種方法保證了用戶能夠及時獲得最新的信息。

將微任務合併到應用程序中(例如本例)可以減輕用戶界面 (UI) 的卡頓或稱為“卡頓”的中斷,從而為用戶帶來更快速、無縫的交互。

事件循環對 Web 開發的影響

深入理解事件循環並利用其功能對於構建高性能和反應式軟件系統至關重要。事件循環提供異步和並發功能,從而允許在應用程序中明智地執行複雜的操作,同時保持最佳的用戶體驗。

Node.js 提供了一個全面的解決方案,其中包含可促進主要 JavaScript 執行上下文之外的額外並行性的 Web Worker。