您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Node.js事件循環是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Node.js事件循環是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
首先我們需要了解一下最基礎的一些東西,比如這個事件循環,事件循環是指Node.js執行非阻塞I/O操作,盡管==JavaScript是單線程的==,但由于大多數==內核都是多線程==的,Node.js
會盡可能將操作裝載到系統內核。因此它們可以處理在后臺執行的多個操作。當其中一個操作完成時,內核會告訴Node.js
,以便Node.js
可以將相應的回調添加到輪詢隊列中以最終執行。
當Node.js啟動時會初始化event loop
, 每一個event loop
都會包含按如下順序六個循環階段:
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
1. timers
階段: 這個階段執行 setTimeout(callback)
和 setInterval(callback)
預定的 callback;
2. I/O callbacks
階段: 此階段執行某些系統操作的回調,例如TCP錯誤的類型。 例如,如果TCP套接字在嘗試連接時收到 ECONNREFUSED,則某些* nix系統希望等待報告錯誤。 這將操作將等待在==I/O回調階段==執行;
3. idle, prepare
階段: 僅node內部使用;
4. poll
階段: 獲取新的I/O事件, 例如操作讀取文件等等,適當的條件下node將阻塞在這里;
5. check
階段: 執行 setImmediate()
設定的callbacks;
6. close callbacks
階段: 比如 socket.on(‘close’, callback)
的callback會在這個階段執行;
這個圖是整個 Node.js 的運行原理,從左到右,從上到下,Node.js 被分為了四層,分別是 應用層
、V8引擎層
、Node API層
和 LIBUV層
。
應用層: 即 JavaScript 交互層,常見的就是 Node.js 的模塊,比如 http,fs
V8引擎層: 即利用 V8 引擎來解析JavaScript 語法,進而和下層 API 交互
NodeAPI層: 為上層模塊提供系統調用,一般是由 C 語言來實現,和操作系統進行交互 。
LIBUV層: 是跨平臺的底層封裝,實現了 事件循環、文件操作等,是 Node.js 實現異步的核心 。
timers
階段 一個timer指定一個下限時間而不是準確時間,在達到這個下限時間后執行回調。在指定時間過后,timers會盡可能早地執行回調,但系統調度或者其它回調的執行可能會延遲它們。
注意:技術上來說,poll 階段控制 timers 什么時候執行。
注意:這個下限時間有個范圍:[1, 2147483647],如果設定的時間不在這個范圍,將被設置為1。
I/O callbacks
階段 這個階段執行一些系統操作的回調。比如TCP錯誤,如一個TCP socket在想要連接時收到ECONNREFUSED, 類unix系統會等待以報告錯誤,這就會放到 I/O callbacks 階段的隊列執行. 名字會讓人誤解為執行I/O回調處理程序, 實際上I/O回調會由poll階段處理.
poll
階段 poll 階段有兩個主要功能:(1)執行下限時間已經達到的timers的回調,(2)然后處理 poll 隊列里的事件。 當event loop進入 poll 階段,并且 沒有設定的 timers(there are no timers scheduled),會發生下面兩件事之一:
如果 poll 隊列不空,event loop會遍歷隊列并同步執行回調,直到隊列清空或執行的回調數到達系統上限;
如果 poll 隊列為空,則發生以下兩件事之一:
如果代碼已經被setImmediate()設定了回調, event loop將結束 poll 階段進入 check 階段來執行 check 隊列(里面的回調 callback)。
如果代碼沒有被setImmediate()設定回調,event loop將阻塞在該階段等待回調被加入 poll 隊列,并立即執行。
但是,當event loop進入 poll 階段,并且 有設定的timers,一旦 poll 隊列為空(poll 階段空閑狀態): event loop將檢查timers,如果有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段,并執行 timer 隊列。
check
階段 這個階段允許在 poll 階段結束后立即執行回調。如果 poll 階段空閑,并且有被setImmediate()設定的回調,event loop會轉到 check 階段而不是繼續等待。
setImmediate() 實際上是一個特殊的timer,跑在event loop中一個獨立的階段。它使用libuv
的API 來設定在 poll 階段結束后立即執行回調。
通常上來講,隨著代碼執行,event loop終將進入 poll 階段,在這個階段等待 incoming connection, request 等等。但是,只要有被setImmediate()設定了回調,一旦 poll 階段空閑,那么程序將結束 poll 階段并進入 check 階段,而不是繼續等待 poll 事件們 (poll events)。
close callbacks
階段 如果一個 socket 或 handle 被突然關掉(比如 socket.destroy()),close事件將在這個階段被觸發,否則將通過process.nextTick()觸發
這里呢,我們通過偽代碼來說明一下,這個流程:
// 事件循環本身相當于一個死循環,當代碼開始執行的時候,事件循環就已經啟動了 // 然后順序調用不同階段的方法 while(true){ // timer階段 timer() // I/O callbacks階段 IO() // idle階段 IDLE() // poll階段 poll() // check階段 check() // close階段 close() } // 在一次循環中,當事件循環進入到某一階段,加入進入到check階段,突然timer階段的事件就緒,也會等到當前這次循環結束,再去執行對應的timer階段的回調函數 // 下面看這里例子 const fs = require('fs') // timers階段 const startTime = Date.now(); setTimeout(() => { const endTime = Date.now() console.log(`timers: ${endTime - startTime}`) }, 1000) // poll階段(等待新的事件出現) const readFileStart = Date.now(); fs.readFile('./Demo.txt', (err, data) => { if (err) throw err let endTime = Date.now() // 獲取文件讀取的時間 console.log(`read time: ${endTime - readFileStart}`) // 通過while循環將fs回調強制阻塞5000s while(endTime - readFileStart < 5000){ endTime = Date.now() } }) // check階段 setImmediate(() => { console.log('check階段') }) /*控制臺打印check階段read time: 9timers: 5008通過上述結果進行分析,1.代碼執行到定時器setTimeOut,目前timers階段對應的事件列表為空,在1000s后才會放入事件2.事件循環進入到poll階段,開始不斷的輪詢監聽事件3.fs模塊異步執行,根據文件大小,可能執行時間長短不同,這里我使用的小文件,事件大概在9s左右4.setImmediate執行,poll階段暫時未監測到事件,發現有setImmediate函數,跳轉到check階段執行check階段事件(打印check階段),第一次時間循環結束,開始下一輪事件循環5.因為時間仍未到定時器截止時間,所以事件循環有一次進入到poll階段,進行輪詢6.讀取文件完畢,fs產生了一個事件進入到poll階段的事件隊列,此時事件隊列準備執行callback,所以會打印(read time: 9),人工阻塞了5s,雖然此時timer定時器事件已經被添加,但是因為這一階段的事件循環為完成,所以不會被執行,(如果這里是死循環,那么定時器代碼永遠無法執行)7.fs回調阻塞5s后,當前事件循環結束,進入到下一輪事件循環,發現timer事件隊列有事件,所以開始執行 打印timers: 5008ps:1.將定時器延遲時間改為5ms的時候,小于文件讀取時間,那么就會先監聽到timers階段有事件進入,從而進入到timers階段執行,執行完畢繼續進行事件循環check階段timers: 6read time: 50082.將定時器事件設置為0ms,會在進入到poll階段的時候發現timers階段已經有callback,那么會直接執行,然后執行完畢在下一階段循環,執行check階段,poll隊列的回調函數timers: 2check階段read time: 7 */
我們來看一個簡單的EventLoop
的例子:
const fs = require('fs'); let counts = 0; // 定義一個 wait 方法 function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } // 讀取本地文件 操作IO function asyncOperation (callback) { fs.readFile(__dirname + '/' + __filename, callback); } const lastTime = Date.now(); // setTimeout setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); // process.nextTick process.nextTick(() => { // 進入event loop // timers階段之前執行 wait(20); asyncOperation(() => { console.log('poll'); }); }); /** * timers 21ms * poll */
這里呢,為了讓這個setTimeout
優先于fs.readFile
回調, 執行了process.nextTick
, 表示在進入timers
階段前, 等待20ms
后執行文件讀取.
nextTick
與 setImmediate
process.nextTick
不屬于事件循環的任何一個階段,它屬于該階段與下階段之間的過渡, 即本階段執行結束, 進入下一個階段前, 所要執行的回調。有給人一種插隊的感覺.
setImmediate
的回調處于check階段, 當poll階段的隊列為空, 且check階段的事件隊列存在的時候,切換到check階段執行,參考nodejs進階視頻講解:進入學習
由于nextTick具有插隊的機制,nextTick的遞歸會讓事件循環機制無法進入下一個階段. 導致I/O處理完成或者定時任務超時后仍然無法執行, 導致了其它事件處理程序處于饑餓狀態. 為了防止遞歸產生的問題, Node.js 提供了一個 process.maxTickDepth (默認 1000)。
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function nextTick () { process.nextTick(() => { wait(20); console.log('nextTick'); nextTick(); }); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); nextTick();
此時永遠無法跳到timer
階段去執行setTimeout里面的回調方法
, 因為在進入timers
階段前有不斷的nextTick
插入執行. 除非執行了1000次到了執行上限,所以上面這個案例會不斷地打印出nextTick
字符串
setImmediate
如果在一個I/O周期
內進行調度,setImmediate() 將始終在任何定時器(setTimeout、setInterval)之前執行.
setTimeout
與 setImmediate
setImmediate()被設計在 poll 階段結束后立即執行回調;
setTimeout()被設計在指定下限時間到達后執行回調;
無 I/O 處理情況下:
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
執行結果:
C:\Users\92809\Desktop\node_test>node test.js timeout immediate C:\Users\92809\Desktop\node_test>node test.js timeout immediate C:\Users\92809\Desktop\node_test>node test.js timeout immediate C:\Users\92809\Desktop\node_test>node test.js immediate timeout
從結果,我們可以發現,這里打印輸出出來的結果,并沒有什么固定的先后順序,偏向于隨機,為什么會發生這樣的情況呢?
答:首先進入的是timers
階段,如果我們的機器性能一般,那么進入timers
階段,1ms
已經過去了 ==(setTimeout(fn, 0)等價于setTimeout(fn, 1))==,那么setTimeout
的回調會首先執行。
如果沒有到1ms
,那么在timers
階段的時候,下限時間沒到,setTimeout
回調不執行,事件循環來到了poll
階段,這個時候隊列為空,于是往下繼續,先執行了setImmediate()的回調函數,之后在下一個事件循環再執行setTimemout
的回調函數。
問題總結:而我們在==執行啟動代碼==的時候,進入timers
的時間延遲其實是==隨機的==,并不是確定的,所以會出現兩個函數執行順序隨機的情況。
那我們再來看一段代碼:
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
打印結果如下:
C:\Users\92809\Desktop\node_test>node test.js immediate timeout C:\Users\92809\Desktop\node_test>node test.js immediate timeout C:\Users\92809\Desktop\node_test>node test.js immediate timeout # ... 省略 n 多次使用 node test.js 命令 ,結果都輸出 immediate timeout
這里,為啥和上面的隨機timer
不一致呢,我們來分析下原因:
原因如下:fs.readFile
的回調是在poll
階段執行的,當其回調執行完畢之后,poll
隊列為空,而setTimeout
入了timers
的隊列,此時有代碼 setImmediate()
,于是事件循環先進入check
階段執行回調,之后在下一個事件循環再在timers
階段中執行回調。
當然,下面的小案例同理:
setTimeout(() => { setImmediate(() => { console.log('setImmediate'); }); setTimeout(() => { console.log('setTimeout'); }, 0); }, 0);
以上的代碼在timers
階段執行外部的setTimeout
回調后,內層的setTimeout
和setImmediate
入隊,之后事件循環繼續往后面的階段走,走到poll階段
的時候發現隊列為空
,此時有代碼有setImmedate()
,所以直接進入check階段
執行響應回調(==注意這里沒有去檢測timers隊列中是否有成員
到達下限事件,因為setImmediate()優先
==)。之后在第二個事件循環的timers
階段中再去執行相應的回調。
綜上所演示,我們可以總結如下:
如果兩者都在主模塊中調用,那么執行先后取決于進程性能,也就是你的電腦好撇,當然也就是隨機。
如果兩者都不在主模塊調用(被一個異步操作包裹),那么**setImmediate的回調永遠先執行
**。
nextTick
與 Promise
概念:對于這兩個,我們可以把它們理解成一個微任務。也就是說,它其實不屬于事件循環的一部分。 那么他們是在什么時候執行呢? 不管在什么地方調用,他們都會在其所處的事件循環最后,事件循環進入下一個循環的階段前執行。
setTimeout(() => { console.log('timeout0'); new Promise((resolve, reject) => { resolve('resolved') }).then(res => console.log(res)); new Promise((resolve, reject) => { setTimeout(()=>{ resolve('timeout resolved') }) }).then(res => console.log(res)); process.nextTick(() => { console.log('nextTick1'); process.nextTick(() => { console.log('nextTick2'); }); }); process.nextTick(() => { console.log('nextTick3'); }); console.log('sync'); setTimeout(() => { console.log('timeout2'); }, 0); }, 0);
控制臺打印如下:
C:\Users\92809\Desktop\node_test>node test.js timeout0 sync nextTick1 nextTick3 nextTick2 resolved timeout2 timeout resolved
最總結:timers
階段執行外層setTimeout
的回調,遇到同步代碼先執行,也就有timeout0
、sync
的輸出。遇到process.nextTick
及Promise
后入微任務隊列,依次nextTick1
、nextTick3
、nextTick2
、resolved
入隊后出隊輸出。之后,在下一個事件循環的timers
階段,執行setTimeout
回調輸出timeout2
以及微任務Promise
里面的setTimeout
,輸出timeout resolved
。(這里要說明的是 微任務nextTick
優先級要比Promise
要高)
代碼片段1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); /* C:\Users\92809\Desktop\node_test>node test.js setImmediate nextTick 嵌套setImmediate*/
解析:
事件循環check
階段執行回調函數輸出setImmediate
,之后輸出nextTick
。嵌套的setImmediate
在下一個事件循環的check
階段執行回調輸出嵌套的setImmediate
。
代碼片段2:
async function async1(){ console.log('async1 start') await async2() console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start') setTimeout(function(){ console.log('setTimeout0') },0) setTimeout(function(){ console.log('setTimeout3') },3) setImmediate(() => console.log('setImmediate')); process.nextTick(() => console.log('nextTick')); async1(); new Promise(function(resolve){ console.log('promise1') resolve(); console.log('promise2') }).then(function(){ console.log('promise3') }) console.log('script end')
打印結果為:
C:\Users\92809\Desktop\node_test>node test.js script start async1 start async2 promise1 promise2 script end nextTick promise3 async1 end setTimeout0 setTimeout3 setImmediate
讀到這里,這篇“Node.js事件循環是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。