Node.js - 事件循環:揭開非同步JavaScript的神秘面紗

你好,未來的編程魔法師們!今天,我們將踏上一段令人興奮的旅程,深入了解Node.js的核心——事件循環(Event Loop)。別擔心如果你之前從未寫過一行代碼;我將成為你進入這個迷人世界的友好導遊。在這個教學結束時,你將會理解Node.js是如何同時處理許多事情,就像你同時應對功課、Netflix和發短信給朋友一樣!

Node.js - Event Loop

什麼是事件循環?

想像你是一家忙碌餐廳廚房中的廚師。你同時煮著多道菜,計時器滴答作響,訂單不斷湧入。你如何在不燒焦食物或不讓顧客等待的情況下管理一切?這正是事件循環對Node.js所做的!

事件循環就像一位總廚,不斷檢查什麼需要關注,並確保一切運行順暢。它是Node.js能夠進行非阻塞I/O操作的秘訣,儘管JavaScript是單線程的。

關鍵概念

在我們深入探討之前,讓我們熟悉一些關鍵概念:

  1. 單線程:JavaScript在單一線程上運行,意味著它一次只能做一件事。
  2. 非阻塞:Node.js可以處理多個操作,而不需要等待每個操作完成才移動到下一個。
  3. 非同步:任務可以立即開始,稍後完成,這樣其他代碼就可以在這段時間內運行。

事件循環是如何工作的?

讓我們將事件循環分解成容易理解的步驟:

  1. 在調用堆疊中執行同步代碼
  2. 檢查計時器(setTimeout, setInterval)
  3. 檢查懸而未決的I/O操作
  4. 執行setImmediate回調
  5. 處理'close'事件

現在,讓我們通過一些代碼示例來看看這是怎麼工作的!

示例 1:同步與非同步代碼

console.log("First");

setTimeout(() => {
console.log("Second");
}, 0);

console.log("Third");

你認為輸出會是什麼?讓我們分析一下:

  1. "First"會立即被記錄。
  2. 遇到setTimeout,但Node.js不會等待,而是設定計時器後繼續執行。
  3. "Third"被記錄。
  4. 事件循環檢查完成的計時器並執行回調,記錄"Second"。

輸出:

First
Third
Second

驚訝嗎?這展示了Node.js是如何在不阻塞主線程的情況下處理非同步操作的。

示例 2:多個計時器

setTimeout(() => console.log("Timer 1"), 0);
setTimeout(() => console.log("Timer 2"), 0);
setTimeout(() => console.log("Timer 3"), 0);

console.log("Hello from the main thread!");

在這個例子中,我們設定了多個計時器,延遲為0毫秒。然而,事件循環仍然會在主線程結束後處理它們。

輸出:

Hello from the main thread!
Timer 1
Timer 2
Timer 3

事件循環的階段

現在我們已經看到了事件循環的運作,讓我們更詳細地探索它的階段:

1. 計時器階段

這個階段執行由setTimeout()和setInterval()排程的回調。

setTimeout(() => console.log("I'm a timer!"), 100);
setInterval(() => console.log("I repeat every 1 second"), 1000);

2. 懸而未決的回調階段

在這裡,循環執行在下一個循環迭代中延遲的I/O回調。

3. 空閒、預備階段

僅供內部使用。這裡沒有什麼好看的!

4. �輪詢階段

檢索新的I/O事件並執行與I/O相關的回調。

const fs = require('fs');

fs.readFile('example.txt', (err, data) => {
if (err) throw err;
console.log(data);
});

5. 檢查階段

在這裡調用setImmediate()回調。

setImmediate(() => console.log("I'm immediate!"));

6. 關閉回調階段

一些關閉回調,例如socket.on('close', ...),在這裡被處理。

準備一切

讓我們創造一個更複雜的示例,它利用事件循環的不同方面:

const fs = require('fs');

console.log("Start");

setTimeout(() => console.log("Timeout 1"), 0);
setImmediate(() => console.log("Immediate 1"));

fs.readFile('example.txt', (err, data) => {
console.log("File read complete");
setTimeout(() => console.log("Timeout 2"), 0);
setImmediate(() => console.log("Immediate 2"));
});

console.log("End");

執行順序可能會讓你感到驚訝:

  1. "Start"和"End"會立即被記錄。
  2. 第一個setTimeout和setImmediate被排隊。
  3. 開始文件讀取操作。
  4. 事件循環開始它的周期:
  • 第一個setTimeout回調被執行。
  • 第一個setImmediate回調被執行。
  • 文件讀取完成時,其回調被執行。
  • 在文件讀取回調內,另一個setTimeout和setImmediate被排隊。
  • 第二個setImmediate在第二個setTimeout之前執行。

常見事件循環方法

以下是Node.js中常見的事件循環相關方法:

方法 描述
setTimeout(callback, delay) 在延遲毫秒後執行回調
setInterval(callback, interval) 每個間隔毫秒執行回調一次
setImmediate(callback) 在事件循環的下一個迭代中執行回調
process.nextTick(callback) 將回調添加到"下一個滴答隊列"中,在當前操作完成後處理

結論

恭喜你!你剛剛踏入了Node.js及其事件循環的迷人世界。記住,就像學騎自行車一樣,掌握非同步編程需要練習。如果它立即沒有點醒你——不要氣餒,繼續嘗試,很快你就能像專家一樣寫出非阻塞代碼!

當我們結束時,這裡有一個有趣的比喻:將事件循環想像成旋轉木馬。不同的任務(如計時器、I/O操作和立即回調)就像試圖跳上木馬的孩子们。事件循環不斷旋轉,按照特定的順序接手和放下任務,確保每個人都能得到一次機會,而且木馬從不停止。

繼續編程,保持好奇心,並記住——在Node.js的世界裡,耐心不僅是一種美德,它是一個回調!

Credits: Image by storyset