您好,登錄后才能下訂單哦!
這篇文章主要講解了“Node.js中事件循環的概念是什么”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Node.js中事件循環的概念是什么”吧!
盡管JavaScript是單線程的,但是事件循環盡可能的使用系統內核允許Node.js執行非阻塞I/O操作 盡管大部分現代內核是多線程的,他們可以在后臺處理多線程任務。當一個任務完成時,內核告訴Node.js,然后適當的回調會被加入到循環中執行,這篇文章會進一步詳細的介紹這個話題
當Node.js開始執行時,首先會初始化事件循環,處理提供的輸入腳本(或者放入REPL,本文檔未涉及)這會執行異步 API調用,調度計時器,或調用 process.nextTick(),然后開始處理事件循環
下圖展示了事件循環執行順序的簡化概覽
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
每一個盒子代表著事件循環的一個階段
每一個階段有一個FIFO的隊列 callback 執行,然而每一個階段基于它自己的方式執行,總體來講,當事件循環進入到一個階段里,它將執行當前階段的任何操作,開始執行當前階段隊列中的回調直到隊列完全消耗完或者執行到隊列的最大數據。當隊列消耗完或者達到最大數量,事件循環就會移動到下一個階段。
timers 這個階段執行 setTimeout() 和 setInterval() 的回調
pending callbacks 執行 I/O 回調推遲到下一個循環迭代
idle,prepare 僅在內部使用
poll 檢索新的 I/O 事件;執行 I/O 相關的回調(幾乎所有相關的回調,關閉回調,)
check setImmediate() 會在此階段調用
close callbacks 關閉回調,例如: socket.on('close', ...)
在事件循環的每個過程中,Node.js檢查是否它正在等待異步的I/O和計時器,如果沒有則完全關閉
一個計時器指定一個回調會被執行的臨界點,而不是人們想讓它執行的時間,計時器會在指定的過去時間之后盡可能早的執行,然而,操作系統調度或者其他回調會讓它延遲執行。
從技術角度上講,poll 階段決定了回調何時執行
例如,你設置了一個計時器,100 ms之后執行,然而你的腳本異步讀取了一個文件花費了 95ms
const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } });
當事件循環進入了 poll 階段,是一個空的隊列,(fs.readFile() 還沒有完成),因此它會等待剩余的毫秒數直到最快的計時器閾值到達,當95 ms之后,fs.readFile() 完成了讀文件并且會花費10 ms完成添加到poll 階段并且執行完畢,當回調完成,隊列中沒有回調要執行了,事件循環循環返回到timers 階段,執行計時器的回調。在這個例子中,你會看到計時器被延遲了105 ms之后執行
為了防止 poll 階段阻塞事件循環,libuv(實現了事件循環和平臺上所有的異步行為的C語言庫)在 poll 階段同樣也有一個最大值停止輪訓更多事件
此階段為某些系統操作(例如 TCP 錯誤類型)執行回調。 例如,如果 TCP 套接字在嘗試連接時收到 ECONNREFUSED,則某些 *nix 系統希望等待報告錯誤。 這將在掛起的回調階段排隊執行。
poll 階段有兩個主要的功能
計算 I/O 阻塞的時間
執行 poll 隊列中的事件
當事件循環進入到了poll階段并且沒有計時器,發生以下兩種事情
如果 poll 隊列中不為空,事件循環會同步地迭代執行每個回調直到執行所有,或者達到系統的的硬限制
如果 poll 隊列是空的,以下兩種情況會發生
如果是setImmediate的回調,事件循環會結束 poll 階段并進入到 check 階段執行回調
如果不是setImmediate,事件循環會等待回調添加到隊列中,然后立即執行
一旦 poll 隊列是空的,事件循環會檢測計時器是否到時間,如果有,事件循環會到達timers 階段執行計時器回調
此階段允許人們在 poll 階段完成后立即執行回調。 如果輪詢階段變得空閑并且腳本已使用 setImmediate() 排隊,則事件循環可能會繼續到 check 階段而不是等待。
setImmediate() 實際上是一個特殊的計時器,它在事件循環的單獨階段運行。 它使用一個 libuv API 來安排在 poll 階段完成后執行的回調。
通常,隨著代碼的執行,事件循環最終會到達 poll 階段,它將等待傳入的連接、請求等。但是,如果使用 setImmediate() 安排了回調并且 poll 階段變得空閑,它將結束并繼續 check 階段,而不是等待 poll 事件。
如果一個 socket 或者操作突然被關閉(e.g socket.destroy()),close 事件會被發送到這個階段,否則會通過process.nextTick()發送
setImmediate() 和 setTimeout() 是相似的,但是不同的行為取決于在什么時候被調用
setTimmediate() 在 poll 階段一旦執行完就會執行
setTimeout() 在一小段時間過去之后被執行
每個回調執行的順序依賴他們被調用的上下本環境,如果在同一個模塊被同時調用,那么時間會受到進程性能的限制(這也會被運行在這臺機器的其他應用所影響)
例如,如果我們不在I/O里邊運行下面的腳本,盡管它受進程性能的影響,但是不能夠確定這兩個計時器的執行順序:
// timeout_vs_immediate.js setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
然而,如果你移動到I/O 循環中,immediate 回調總是會先執行
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
$ node timeout_vs_immediate.js immediate timeout $ node timeout_vs_immediate.js immediate timeout
setImmediate 相對于 setTimeout 的優勢是 setImmediate 如果在I/O 中總是會優先于任何計時器被先執行,與存在多少計時器無關。
盡管 process.nextTick() 是異步API的一部分,但是你可能已經注意到了它沒有出現在圖表中,這是因為 process.nextTick() 不是事件循環技術的一部分,相反,當前操作執行完畢之后 nextTickQueue 會被執行,無論事件循環的當前階段如何。 在這里,操作被定義為來自底層 C/C++ 處理程序的轉換,并處理需要執行的 JavaScript。 根據圖表,你可以在任意階段調用 process.nextTick(),在事件循環繼續執行之前,所有傳遞給 process.nextTick() 的回調都將被執行,這個會導致一些壞的情況因為它允許你遞歸調用 process.nextTick() "starve" 你的 I/O ,這會阻止事件循環進入 poll 階段。
為什么這種情況會被包含在Node.js中?因為Node.js的設計理念是一個API應該總是異步的即使它不必須,看看下面的片段
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick( callback, new TypeError('argument should be string') ); }
該片段會進行參數檢查,如果不正確,它會將錯誤傳遞給回調。 API 最近更新,允許將參數傳遞給 process.nextTick() 允許它接受在回調之后傳遞的任何參數作為回調的參數傳播,因此您不必嵌套函數。
我們正在做的是將錯誤傳回給用戶,但前提是我們允許用戶的其余代碼執行。 通過使用 process.nextTick(),我們保證 apiCall() 總是在用戶代碼的其余部分之后和允許事件循環繼續之前運行它的回調。 為了實現這一點,允許 JS 調用堆棧展開,然后立即執行提供的回調,這允許人們對 process.nextTick() 進行遞歸調用,而不會達到 RangeError:從 v8 開始超出最大調用堆棧大小。
感謝各位的閱讀,以上就是“Node.js中事件循環的概念是什么”的內容了,經過本文的學習后,相信大家對Node.js中事件循環的概念是什么這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。