您好,登錄后才能下訂單哦!
這篇文章主要介紹了nodejs中事件循環機制的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
前端開發離不開JavaScript,Javascript是一種web前端語言,主要用于web開發中,由瀏覽器解析執行。而js的作用不僅僅局限于前端領域的開發,它同樣可以用于服務端開發——nodejs。作為一名有理想抱負的前端,想要拓展視野,掌握一門服務器端開發語言,那么nodejs是非常好的一種選擇。
因為你掌握了js開發方式就很容易上手node,并且npm包管理工具也大大提升了開發體驗。nodejs以異步非阻塞I/O工作方式而聞名,其處理機制被稱為事件循環。
了解node事件循環機制就能更好的了解node的事件處理方式以及異步事件的執行時機,本文主要講解一下nodejs的事件循環機制,為后續學習node奠定基礎。
前面提到,Javascript是一種web前端語言,主要用于web開發中,由瀏覽器解析執行,而 node.js 是一個基于 Chrome V8 引擎的JavaScript 運行環境,因此nodejs不是一門語言,不是庫,不是框架,而是一個js運行時環境,簡單講node可以解析和執行js代碼。以前只有瀏覽器可以解析執行Js,現在node可以使js完全脫離瀏覽器來運行。
node.js和瀏覽器js存在很多區別,比如瀏覽器中的js包括了ecmascript、BOM、DOM,但是nodejs中的js沒有BOM,DOM,只有emcscript。并且node這個js執行環境為js提供了一些服務器級別的操作API,例如:文件讀寫,網絡服務構建,網絡通信,http服務器等,這些API大都被包裝到核心模塊里面了。另外node的事件循環機制和瀏覽器js的事件循環機制也不一樣。
大家對瀏覽器中的js事件循環已經很清楚了,為了對比這里簡單再提一下。
(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)
js執行時同步和異步任務任務分別進入不同的執行環境,同步任務的進入主線程,即主執行棧,異步任務(ajax請求、settimeout、setinterval、poromise.resolve()等)進入任務隊列。不同的異步任務會推入不同的任務隊列,比如ajax請求、settimeout、setinterval等這些任務會被推入進宏任務隊列(Macro Task),而Promise函數則會被推到微任務隊列(Micro Task)。整體的事件循環過程如下:
當同步代碼執行完后,主執行棧變空,開始準備執行異步任務。
主線程會檢查微任務隊列是否為空,如果不為空那么會遍歷隊列內的所有微任務將其執行完,清空微任務隊列,然后再檢查宏任務隊列。如果微任務隊列是空的,直接進入下一步。
主線程遍歷宏任務隊列,并執行宏任務隊列中的第一個宏任務,在執行的過程中如果遇到宏任務或者微任務,則繼續將他們推入到對應的任務隊列,每次執行完一次宏任務都要遍歷執行一下微任務隊列,將其清空
執行渲染操作,更新視圖
開始下一次的事件循環,重復上述步驟直至兩個任務隊列清空
為了加深一下影響,舉一個小小的,看看以下代碼會輸出什么:
var le=Promise.resolve(2); console.log(le) console.log('3') Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0);
用以上的事件循環過程分析一下:
js主進程執行代碼遇到Promise.resolve(2),會立即執行,將2變成一個promise對象,然后console.log(le)將le變量打印出來,打印----->Promise {<fulfilled>: 2};
console.log('3'),打印----->3
接著往下執行遇到Promise.resolve().then,這是一個異步微任務函數,推到微任務棧
下一個函數遇到setTimeout,推到宏任務隊列,至此主進程空了
檢查微任務隊列,發現Promise.resolve().then,所以打印----->promise1,又遇到一個定時器,推至宏任務隊列的最后,微任務隊列空了
檢查宏任務隊列,取第一個宏任務執行,打印----->setTimeout1,又遇到 Promise.resolve().then,推至微任務隊列
再開始下一個宏任務之前,一定會清空微任務,因此打印setTimeout1后,便會檢查微任務隊列,于是--->promise2
接下來又一輪事件循環,取宏任務隊列當前的第一個任務執行,于是打印打印----->setTimeout2,至此宏任務和微任務隊列均被清空,事件循環結束
因此輸出結果是:Promise {<fulfilled>: 2},3,promise1,setTimeout1,promise2,setTimeout2。
瀏覽器里執行結果如下:
node的事件循環共有六個階段,在一次事件循環中這六個階段按順序會一直循環執行,直至事件處理完成。六個階段的順序圖如下:
六個階段分別是:
timers 階段:這個階段執行timer(setTimeout、setInterval)的回調
I/O callbacks 階段:執行一些系統操作的回調(比如網絡通信的錯誤回調);
idle, prepare 階段:僅node內部使用,可忽略
poll 階段:獲取新的I/O事件, 適當的條件下node將阻塞在這里
check 階段:執行 setImmediate() 的回調
close callbacks 階段:執行 socket 的 close 事件回調,如果一個socket或handle被突然關掉(比如socket.destroy()),close事件將在這個階段被觸發
事件循環中,每當進入某一個階段,都會從該階段對應的回調隊列中取出函數去執行。當隊列為空或者執行的回調函數數量到達系統設定的閾值,該階段就會終止,然后檢查NextTick隊列和微任務隊列,將其清空,之后進入下一個階段。
這里面比較關鍵的是poll階段:
poll隊列不為空的時候,事件循環會遍歷隊列并同步執行回調,直到隊列清空或執行回調數達到系統上限。
poll隊列為空的時候,就會有兩種情況:
如果代碼中存在setImmediate()回調,那么事件循環直接結束poll階段進入check階段來執行check隊列里的回調;
如果不存在setImmediate()回調,會等待回調被加入到隊列中并立即執行回調,這里同樣會有個超時時間設置防止一直等待下去,如果規定時間內有定時器函數進入隊列,則返回到timer階段,執行定時器回調,否則在poll階段等待回調進入隊列。
同樣的舉個大大的,看看以下代碼會輸出什么:
console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end')
利用node事件循環分析唄:
先執行同步任務,打印start,end
進入timer階段前,清空NextTick和micro隊列,所以打印promise3
進入timer階段,打印timer1,并發現有一個微任務,立即執行微任務,打印promise1
仍然在timer階段,執行下個宏任務,打印timer2,同樣遇到微任務,立即執行,打印promise2
因此輸出順序是:start,end,promise3,timer1,promise1,timer2,promise2,如果能正確回答出來說明對node的循環機制有了大體的了解,實際node輸出結果確實是這樣:
那如下代碼會輸出什么呢?
process.nextTick(function(){ console.log(7); }); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); }); process.nextTick(function(){ console.log(8); });
繼續分析:
process.nextTick會將任務推進至nextTick隊列,promise.then會把任務推至micro隊列,上面提到過每次一個宏任務執行完,執行下一個宏任務之前需要清空nextTick隊列和micro隊列,同樣的一個階段執行完,進入下一個階段之前也需要nextTick隊列和micro隊列,并且nextTick隊列優先級高于micro隊列
先執行同步代碼,打印3,4
執行nextTick隊列,打印7,8
再執行micro隊列,打印5
因此最終輸出是:3,4,7,8,5,需要記住,process.nextTick 永遠大于 promise.then的優先級
還有一個大家很容易混淆的點就是setTimout和setImmediate的執行時機,根據上面描述的node事件循環機制,setImmediate()應該在check階段執行 與 而setTimeout在timer階段執行,理論上setTimout比setImmediate先執行,看下面的代碼:
setTimeout(() => console.log(1),0); setImmediate(() => console.log(2));
執行結果是什么?1,2 還是 2,1,其實都有可能,看實際node運行的結果:
可以看到兩次執行的結果不一樣,為什么呢?原因在于即使setTimeout的第二個參數默認為0,但實際上,Node做不到0秒就執行其回調,最少也要4毫秒。那么進入事件循環后,如果沒到4毫秒,那么timers階段就會被跳過,從而進入check階段執行setImmediate回調,此時輸出結果是:2,1;
如果進入事件循環后,超過4毫秒(只是個大概,具體值并不確定),setTimeout的回調會出現在timer階段的隊列里,回調將被執行,之后再進入poll階段和check階段,此時輸出結果是:1,2
那如果兩者在I/O周期內調用,誰先執行呢?看一下代碼:
const fs = require('fs') fs.readFile('./test.txt', 'utf8' , (err, data) => { if (err) { console.error(err) return } setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); })
實際上,node中輸出的結果總是immediate先輸出,timeout后輸出。因為I/O回調是在poll階段執行,當回調執行完畢之后隊列為空,發現存在setImmediate的回調就會進入check階段,執行完畢后,再進入timer階段。
感謝你能夠認真閱讀完這篇文章,希望小編分享的“nodejs中事件循環機制的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。