
我們在名為「Node Internals」的文章中討論了為什麼 Node JS 是單線程的,也是多線程的。它將為您提供 Node 架構的堅實基礎,並為理解事件循環的魔力奠定基礎!
由於事件循環,Node js 可以被認為是單線程的。但是,什麼是事件循環呢?
我總是從餐廳的類比開始,因為我認為這樣比較容易理解技術細節。
所以,在餐廳裡,主廚從訂單清單中取出訂單並將其交給助理團隊。食物準備好後,廚師就會上菜。如果有VIP顧客來,廚師會優先處理這個訂單。
如果我們考慮這個類比,那我們可以說......
在 Node JS 事件循環的上下文中。
要了解事件循環,我們必須先了解微任務和巨集任務之間的差異。
微任務
微任務是指具有高優先權的任務,並且在目前執行的 Javascript 程式碼完成之後、進入事件循環的下一階段之前執行。
範例:
- process.nextTick
- 承諾(.then、.catch、.finally)
- 隊列微任務
巨集任務
這些是優先順序較低的任務,在事件循環的稍後階段排隊等待執行。
範例:
事件循環
當我們在 Node.js 中執行非同步任務時,事件循環是一切的核心。
得益於事件循環,Node.js 可以有效率地執行非阻塞 I/O 操作。它透過將耗時的任務委託給作業系統或工作執行緒來實現這一點。一旦任務完成,它們的回調就會以有組織的方式處理,確保順利執行而不阻塞主執行緒。
這就是 Node.js 能夠同時處理多個任務,同時仍然是單執行緒的神奇之處。
階段
事件循環中有六個階段,每個階段都有自己的佇列,其中保存特定類型的任務。
1.計時器階段
在此階段處理與計時器相關的回調,例如 setTimeout 和 setInterval。
Node js 檢查計時器佇列中是否有延遲已過期的回呼。
如果滿足計時器延遲,其回調將會加入此佇列中執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | console.log( 'Start' );
setTimeout(() => {
console.log( 'Timer 1 executed after 1 second' );
}, 1000);
setTimeout(() => {
console.log( 'Timer 2 executed after 0.5 seconds' );
}, 500);
let count = 0;
const intervalId = setInterval(() => {
console.log( 'Interval callback executed' );
count ++;
if ( count === 3) {
clearInterval(intervalId);
console.log( 'Interval cleared' );
}
}, 1000);
console.log( 'End' );
|
登入後複製
登入後複製
輸出:
1 2 3 4 5 6 7 8 | Start
End
Timer 2 executed after 0.5 seconds
Timer 1 executed after 1 second
Interval callback executed
Interval callback executed
Interval callback executed
Interval cleared
|
登入後複製
登入後複製
2.I/O回呼階段
此階段的目的是為已完成的 I/O(輸入/輸出)操作執行回調,例如讀取或寫入檔案、查詢資料庫、處理網路請求以及其他非同步 I/O 任務。
當 Node.js 中發生任何非同步 I/O 操作(例如使用 fs.readFile 讀取檔案)時,該操作將委託給作業系統或工作執行緒。這些 I/O 任務在主執行緒之外以非阻塞方式執行。任務完成後,會觸發回呼函數來處理結果。
I/O 回呼階段是操作完成後這些回呼排隊等待執行的階段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const fs = require ( 'fs' );
console.log( 'Start' );
fs.readFile( 'example.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file:' , err);
return ;
}
console.log( 'File contents:' , data);
});
console.log( 'Middle' );
setTimeout(() => {
console.log( 'Simulated network request completed' );
}, 0);
console.log( 'End' );
|
登入後複製
登入後複製
輸出
1 2 3 4 5 | Start
Middle
End
Simulated network request completed
File contents: (contents of the example.txt file)
|
登入後複製
登入後複製
3.空閒階段
在此階段,不會執行任何使用者定義的工作,而是在此階段事件循環為下一階段做好準備。此階段僅進行內部調整。
4.投票階段
輪詢階段檢查是否有需要處理的待處理 I/O 事件(如網路活動或檔案系統事件)。它將立即執行與這些事件相關的回調。
如果沒有待處理的 I/O 事件,則輪詢階段可以進入阻塞狀態。
在這種阻塞狀態下,Node.js 將只是等待新的 I/O 事件到達。這種阻塞狀態使 Node.js 成為非阻塞:它會等待,直到新的 I/O 事件觸發回呼執行,同時保持主執行緒空閒以執行其他任務。
已完成的 I/O 操作(例如 fs.readFile、HTTP 請求或資料庫查詢)的任何回呼都會在此階段執行。這些 I/O 操作可能已在先前的階段(例如計時器階段或 I/O 回呼階段)啟動,現在已完成。
如果有使用 setTimeout 或 setInterval 設定的計時器,Node.js 將檢查是否有計時器已過期以及是否需要執行其關聯的回呼。如果計時器已過期,它們的回呼將移至回呼佇列,但直到下一階段(即計時器階段)才會處理它們。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | const fs = require ( 'fs' );
const https = require ( 'https' );
console.log( 'Start' );
fs.readFile( 'file1.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file1:' , err);
return ;
}
console.log( 'File1 content:' , data);
});
fs.readFile( 'file2.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file2:' , err);
return ;
}
console.log( 'File2 content:' , data);
});
https.get( 'https://jsonplaceholder.typicode.com/todos/1' , (response) => {
let data = '' ;
response.on( 'data' , (chunk) => {
data += chunk;
});
response.on( 'end' , () => {
console.log( 'HTTP Response:' , data);
});
});
console.log( 'End' );
|
登入後複製
登入後複製
輸出:
1 2 3 4 5 | Start
End
File1 content: (contents of file1.txt)
File2 content: (contents of file2.txt)
HTTP Response: (JSON data from the HTTP request)
|
登入後複製
登入後複製
5.檢查相位
投票階段完成任務後。此階段主要處理 setImmediate 回呼的執行,這些回呼被安排在輪詢階段處理完 I/O 事件後立即執行。
當您想要在目前事件循環週期之後執行某個操作時,通常會使用 setImmediate 回調,例如確保系統不忙於處理 I/O 事件後執行某些任務。
檢查階段的優先權高於定時器階段(處理 setTimeout 和 setInterval)。這表示 setImmediate 回呼將始終在任何計時器之前執行,即使計時器已過期。
setImmediate 保證其回呼將在當前 I/O 週期之後、下一個計時器週期之前運行。當您想要確保在執行其他任務之前先完成 I/O 相關任務時,這一點非常重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | console.log( 'Start' );
setTimeout(() => {
console.log( 'Timer 1 executed after 1 second' );
}, 1000);
setTimeout(() => {
console.log( 'Timer 2 executed after 0.5 seconds' );
}, 500);
let count = 0;
const intervalId = setInterval(() => {
console.log( 'Interval callback executed' );
count ++;
if ( count === 3) {
clearInterval(intervalId);
console.log( 'Interval cleared' );
}
}, 1000);
console.log( 'End' );
|
登入後複製
登入後複製
輸出:
1 2 3 4 5 6 7 8 | Start
End
Timer 2 executed after 0.5 seconds
Timer 1 executed after 1 second
Interval callback executed
Interval callback executed
Interval callback executed
Interval cleared
|
登入後複製
登入後複製
6.結束階段
關閉回呼階段通常在應用程式需要在退出或關閉之前進行清理時執行。
此階段處理不再需要係統資源(例如網路套接字或檔案句柄)時需要執行的事件和任務。
如果沒有此階段,應用程式可能會留下開啟的檔案句柄、網路連接或其他資源,可能導致記憶體洩漏、資料損壞或其他問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const fs = require ( 'fs' );
console.log( 'Start' );
fs.readFile( 'example.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file:' , err);
return ;
}
console.log( 'File contents:' , data);
});
console.log( 'Middle' );
setTimeout(() => {
console.log( 'Simulated network request completed' );
}, 0);
console.log( 'End' );
|
登入後複製
登入後複製
輸出:
1 2 3 4 5 | Start
Middle
End
Simulated network request completed
File contents: (contents of the example.txt file)
|
登入後複製
登入後複製
Node JS 的事件循環中還有一個特殊的階段。
微任務隊列
process.nextTick() 並承諾在事件循環的特殊階段執行回呼。
process.nextTick() 安排回呼在目前操作完成後立即執行,但在事件循環繼續下一階段之前。
process.nextTick() 不是事件循環中任何階段的一部分。相反,它有自己的內部佇列,該佇列在當前執行的同步程式碼之後和進入事件循環中的任何階段之前立即執行。
它在目前操作之後但在 I/O、setTimeout 或事件循環中安排的其他任務之前執行。
Promise 的優先權低於 process.nextTick(),並且在所有 process.nextTick() 回呼之後處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | const fs = require ( 'fs' );
const https = require ( 'https' );
console.log( 'Start' );
fs.readFile( 'file1.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file1:' , err);
return ;
}
console.log( 'File1 content:' , data);
});
fs.readFile( 'file2.txt' , 'utf8' , (err, data) => {
if (err) {
console.log( 'Error reading file2:' , err);
return ;
}
console.log( 'File2 content:' , data);
});
https.get( 'https://jsonplaceholder.typicode.com/todos/1' , (response) => {
let data = '' ;
response.on( 'data' , (chunk) => {
data += chunk;
});
response.on( 'end' , () => {
console.log( 'HTTP Response:' , data);
});
});
console.log( 'End' );
|
登入後複製
登入後複製
輸出:
1 2 3 4 5 | Start
End
File1 content: (contents of file1.txt)
File2 content: (contents of file2.txt)
HTTP Response: (JSON data from the HTTP request)
|
登入後複製
登入後複製
現在,您對事件循環的工作原理有了總體了解。
我給你一個問題,你可以在評論中給出答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const fs = require ( 'fs' );
console.log( 'Start' );
fs.readFile( 'somefile.txt' , 'utf8' , (err, data) => {
if (err) {
console.error(err);
return ;
}
console.log( 'File content:' , data);
});
setImmediate(() => {
console.log( 'Immediate callback executed' );
});
setTimeout(() => {
console.log( 'Timeout callback executed' );
}, 0);
console.log( 'End' );
|
登入後複製
謝謝。
等待您的答覆。
以上是Node JS - 事件循環的詳細內容。更多資訊請關注PHP中文網其他相關文章!