網站搜尋

javascript - 了解事件循環


我正在思考這件事,這是我想出來的:

讓我們看看下面的程式碼:

console.clear();
console.log("a");
setTimeout(function(){console.log("b");},1000);
console.log("c");
setTimeout(function(){console.log("d");},0);

一個請求進來,JS 引擎開始逐步執行上面的程式碼。前兩個呼叫是同步呼叫。但當遇到 setTimeout 方法時,它變成非同步執行。但 JS 會立即從它返回並繼續執行,這稱為 非阻塞 或 異步。然後它繼續處理其他等。

這次執行的結果如下:

a c d b

所以基本上第二個 setTimeout 先完成了,它的回呼函數比第一個先執行,這是合理的。

我們這裡討論的是單線程應用程式。JS 引擎會持續執行這個,除非它完成第一個請求,否則不會處理第二個請求。但好處是它不會等待像 setTimeout 這樣的阻塞操作完成,因此會更快,因為它可以接受新的進來的請求。

但是我的問題圍繞以下幾項提出:

#1: 如果我們在討論單執行緒應用程式,那麼是什麼機制在 JS 引擎接受更多請求並執行它們時處理 setTimeout?單執行緒如何持續處理其他請求?在其他請求持續進來並被執行時,是什麼在處理 setTimeout。

#2: 如果這些 setTimeout 函數在背後被執行,同時更多的請求進來並被執行,那麼在背後進行異步執行的是什麼?我們所說的 EventLoop 指的是這個東西嗎?

#3: 但是整個方法不應該放在 EventLoop 裡,這樣整個過程才會被執行,回調方法才會被調用嗎?這是我在談論回調函數時的理解:

function downloadFile(filePath, callback)
{
   blah.downloadFile(filePath);
   callback();
}

但在這種情況下,JS 引擎如何知道它是一個異步函數,以便可以將回調放入 EventLoop?也許類似於 C# 中的 async 關鍵字,或某種屬性,用於指示 JS 引擎該方法是一個異步方法,應該相應地處理。

#4: 但有一篇文章說的內容與我原本猜測事情可能運作的方式完全相反:

事件循環是一個回調函數的隊列。當異步函數執行時,回調函數會被推入隊列。JavaScript 引擎不會開始處理事件循環,直到異步函數之後的代碼執行完畢。

#5: 這裡有一張圖片可能會有幫助,但圖片中的第一個解釋說的正是第四題中提到的內容:

所以我在這裡的問題是想針對上述列出的項目獲得一些澄清?

答案#1

1: 如果我們在談論單線程應用程式的話,那麼是什麼進程在 JS 引擎接受更多請求並執行它們的同時設置了 setTimeout 呢?單線程不會繼續處理其他請求嗎?那麼在其他請求不斷到來並被執行的同時,誰會繼續處理 setTimeout 呢。

在 Node.js 的進程中,實際上只有一個線程會執行你的程式的 JavaScript。然而,在 Node 本身內部,實際上有幾個線程在處理事件循環機制的運作,這其中包括一個 IO 線程池和其他一些線程。關鍵在於,這些線程的數量並不對應於像每連線一線程的併發模型中那樣的並行連線數。

現在關於「執行 setTimeout」,當你調用 setTimeout 時,Node 所做的基本上只是更新一個資料結構,這個結構裡存放將來要執行的函數。它基本上有很多需要執行的任務佇列,而事件循環的每一次「滴答」都會選擇其中一個,將其從佇列中移除,然後執行它。

一個關鍵的事情是要理解 node 在大多數重度操作上依賴作業系統。因此,進來的網路請求實際上是由作業系統自身追蹤的,而當 node 準備好處理時,它只是使用系統呼叫向作業系統請求一個已經準備好可以處理的網路請求。因此,node 執行的大部分 IO「工作」要麼是「嘿,作業系統,有網路連線的資料準備好可以讀取了嗎?」要麼是「嘿,作業系統,我有沒有任何未完成的檔案系統呼叫資料已經準備好了?」。基於其內部算法和事件迴圈引擎設計,node 將會選擇一個 JavaScript 的「tick」來執行,運行它,然後重複這個過程。 這就是事件迴圈的意思。Node 基本上是在任何時候都在決定「我接下來應該執行哪一小段 JavaScript?」然後執行它。這考慮到作業系統已完成的 IO,以及通過調用 setTimeout 或 process.nextTick 在 JavaScript 中排隊的任務。

2:如果這些 setTimeout 會在更多請求進來並被執行的同時在後臺執行,那麼在後臺執行異步操作的東西就是我們正在討論的 EventLoop 嗎?

沒有 JavaScript 在幕後執行。你程式中的所有 JavaScript 都是前台執行,一次一個。幕後發生的事情是作業系統處理 IO,而 Node 等待 IO 準備好,並且 Node 管理它等待執行的 JavaScript 隊列。

3:JS 引擎如何知道它是否是一個異步函數,以便將其放入事件循環?

在 Node 核心中有一組固定的函數是非同步的,因為它們會進行系統呼叫,而 Node 知道哪些函數是非同步的,因為它們必須呼叫作業系統或 C++。基本上,所有網路和檔案系統的 I/O,以及子程序的互動,都是非同步的,而 JavaScript 能讓 Node 執行非同步操作的唯一方式,就是呼叫 Node 核心庫提供的非同步函數。即使你正在使用定義自身 API 的 npm 套件,為了讓事件循環得以釋放,最終該 npm 套件的程式碼也會呼叫 Node 核心的某個非同步函數,這時 Node 才知道該 tick 已完成,並可以再次啟動事件循環演算法。

4 事件循環是一個回呼函數的隊列。當一個非同步函數執行時,回呼函數會被加入隊列中。JavaScript 引擎直到非同步函數之後的程式碼執行完畢後,才開始處理事件循環。

是的,這是真的,但這具有誤導性。關鍵是正常模式是:

//Let's say this code is running in tick 1
fs.readFile("/home/barney/colors.txt", function (error, data) {
  //The code inside this callback function will absolutely NOT run in tick 1
  //It will run in some tick >= 2
});
//This code will absolutely also run in tick 1
//HOWEVER, typically there's not much else to do here,
//so at some point soon after queueing up some async IO, this tick
//will have nothing useful to do so it will just end because the IO result
//is necessary before anything useful can be done

所以沒錯,你完全可以透過同步計算費波那契數列,將所有計算都存在記憶體裡,並且在同一個時刻執行,來完全阻塞事件循環,而這確實會讓你的程式完全凍結。這就是協作式並發。JavaScript 的每一個時刻都必須在合理的時間內釋放事件循環,否則整體架構就會失效。

答案#2

不要認為主機進程是單線程的,它們不是。單線程的是主機進程中執行你的 JavaScript 代碼的部分。

除了背景工作者,但這些會使情況複雜...

所以,你所有的 js 代碼都在同一個執行緒運行,並且沒有可能讓你兩個不同的 js 代碼片段同時運行(所以,你不用管理並發的噩夢)。

正在執行的 js 代碼是主機進程從事件循環中獲取的最後一段代碼。在你的代碼中,你基本上可以做兩件事:執行同步指令,以及安排函數在未來某些事件發生時執行。

這是我對你的範例程式碼的心理表徵(注意:僅僅是這樣,我不知道瀏覽器的實作細節!)

console.clear();                                   //exec sync
console.log("a");                                  //exec sync
setTimeout(                //schedule inAWhile to be executed at now +1 s 
    function inAWhile(){
        console.log("b");
    },1000);    
console.log("c");                                  //exec sync
setTimeout(
    function justNow(){          //schedule justNow to be executed just now
        console.log("d");
},0);       

當你的程式碼運行時,主進程中的另一個執行緒會追蹤所有發生的系統事件(UI 點擊、讀取的檔案、收到的網路封包等)。

當你的程式碼執行完成後,它會從事件迴圈中移除,主程序會回去檢查它,以查看是否有更多程式碼要執行。事件迴圈包含另外兩個事件處理器:一個現在要執行(justNow 函數),另一個在一秒內執行(inAWhile 函數)。

主進程現在嘗試匹配所有發生的事件,以查看是否有為它們註冊的處理程式。它發現 justNow 正在等待的事件已經發生,所以它開始運行其代碼。當 justNow 函數退出時,它再次檢查事件循環,尋找事件的處理程式。假設已經過了 1 秒,它運行 inAWhile 函數,如此類推……