主執行緒從"任務佇列"讀取事件,這個過程是循環不斷的,所以整個的這種運作機制又稱為Event Loop(事件循環)。以下這篇文章就來帶大家掌握Node.js中的eventloop,希望對大家有幫助!
雖然js
可以在瀏覽器中執行又可以在node
中執行,但是它們的事件循環機制並不是一樣的。並且有很大的差別。
在說Node
#事件循環機制之前,我們先討論兩個問題
學習事件循環可以讓開發者明白JavaScript
的運作機制是怎麼樣的。
事件循環機制用於管理非同步API的回呼函數何時回到主執行緒執行。
Node.js採用的是非同步IO模型。同步API在主執行緒中執行,非同步API在底層的C 維護的執行緒中執行,非同步API的回呼函數也會在主執行緒中執行。 【相關教學推薦:nodejs影片教學、程式設計教學】
#在Javascript應用程式執行時,眾多非同步API的回呼函數何時能回到主執行緒中調用呢?這就是事件環環機製做的事情,管理非同步API的回呼函數什麼時候會回到主執行緒執行。
在Node
中的事件循環分為六個階段。
在事件循環中的每個階段都有一個佇列,儲存要執行的回呼函數,事件循環機制會按照先進先出的方式執行他們直到隊列為空。
這六個階段都儲存著非同步回呼函數,所以還是遵循先執行主執行緒同步程式碼,當同步程式碼執行完後再來輪詢這六個階段。
接下來,我們來詳細看看這六個階段裡面儲存的都是什麼
##Timers :用於儲存定時器的回呼函數(setlnterval,setTimeout)。
Pendingcallbacks:執行與作業系統相關的回呼函數,例如啟動伺服器端應用程式時監聽連接埠操作的回呼函數就在這裡調用。
idle,prepare:系統內部使用。 (這個我們程式設計師不用管)
Poll:儲存1/O操作的回呼函數佇列,例如檔案讀寫操作的回調函數。
停留一段時間以等待新的回呼函數進入。
但是對於這個等待並不是一定的,而是取決於以下兩個條件:
Check:儲存setlmmediate的回呼函數。
Closingcallbacks:執行與關閉事件相關的回調,例如關閉資料庫連接的回呼函數等。
js一樣,
node中的非同步程式碼也分為巨集任務和微任務,只是它們之間的執行順序有所區別。
Node中都有哪些巨集任務和微任務
在node
#中,微任務的回呼函數被放置在微任務佇列中,巨集任務的回呼函數被放置在巨集任務佇列中。
微任務優先權高於巨集任務。當微任務事件佇列中存在可執行的回呼函數時,事件循環會在執行完目前階段的回呼函數後會暫停進入事件循環的下一個階段,而會立即進入微任務的事件佇列中開始執行回呼函數,當微任務佇列中的回呼函數執行完成後,事件循環才會進入到下一個段開始執行回呼函數。
對於微任務我們還有個點需要特別注意。那就是雖然nextTick
同屬於微任務,但是它的優先權是高於其它微任務,在執行微任務時,只有nextlick
中的所有回調函數執行完成後才會開始執行其它微任務。
總的來說就是當主執行緒同步程式碼執行完畢後會優先清空微任務(如果微任務繼續產生微任務則會再次清空),然後再到下個事件循環階段。而微任務的執行是穿插在事件循環六個階段中間的,也就是每次事件循環進入下個階段前會判斷微任務隊列是否為空,為空才會進入下個階段,否則先清空微任務隊列。
下面我們用程式碼實操來驗證前面所說的。
在Node
應用程式啟動後,並不會立即進入事件循環,而是先執行同步程式碼,從上到下開始執行,同步API立即執行,非同步API交給C 維護的執行緒執行,非同步API的回呼函數被註冊到對應的事件佇列中。當所有同步程式碼執行完成後,才會進入事件循環。
console.log("start"); setTimeout(() => { console.log("setTimeout 1"); }); setTimeout(() => { console.log("setTimeout 2"); }); console.log("end");
我們來看執行結果
可以看到,先執行同步程式碼,然後才會進入事件循環執行非同步程式碼,在 timers
階段執行兩個setTimeout
回呼。
#我們知道setTimeout
是在timers
階段執行, setImmediate
是在check
階段執行。且事件循環是從timers
階段開始的。所以會先執行setTimeout
再執行setImmediate
。
對於上面的分析一定對嗎?
我們來看範例
console.log("start"); setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); }); const sleep = (delay) => { const startTime = +new Date(); while (+new Date() - startTime < delay) { continue; } }; sleep(2000); console.log("end");
執行上面的程式碼,輸出如下
#先執行setTimeout
再執行setImmediate
接下來我們來改造下上面的程式碼,把延遲器去掉,看看會輸出什麼
setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); });
我們運行了七次,可以看到其中有兩次是先跑的setImmediate
怎麼回事呢?不是先timers
階段再到check
階段嗎?怎麼會變呢?
其實這就得看進入事件循環的時候,非同步回呼有沒有完全準備好了。對於最開始的例子,因為有2000毫秒的延遲,所以進入事件循環的時候,setTimeout
回呼是一定準備好了的。所以執行順序不會變。但對於這個例子,因為主執行緒沒有同步程式碼需要執行,所以一開始就進入事件循環,但是在進入事件循環的時候,setTimeout
的回呼並不是一定完全準備好的,所以就會有先到check
階段執行setImmediate
回呼函數,再到下一次事件循環的timers
階段來執行setTimeout
的回呼。
那在什麼情況下同樣的延遲時間,setImmediate
回呼函數一定會優先於setTimeout
的回呼呢?
其實很簡單,只要將這兩者放到timers
階段和check
階段之間的Pendingcallbacks、idle,prepare、poll
階段中任一個階段就可以了。因為這些階段完執行完是一定會先到check
再到timers
階段的。
我們以poll
階段為例,將這兩者寫在IO運算中。
const fs = require("fs"); fs.readFile("./fstest.js", "utf8", (err, data) => { setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); }); });
我們也來執行七次,可以看到,每次都是setImmediate
先執行。
所以總的來說,同樣的延遲時間,setTimeout
並不是百分之百先於setImmediate
執行。
主线程同步代码执行完毕后,会先执行微任务再执行宏任务。
我们来看下面的例子
console.log("start"); setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); }); Promise.resolve().then(() => { console.log("Promise.resolve"); }); console.log("end");
我们运行一下看结果,可以看到它是先执行了微任务然后再执行宏任务
在微任务中nextTick
的优先级是最高的。
我们来看下面的例子
console.log("start"); setTimeout(() => { console.log("setTimeout"); }); setImmediate(() => { console.log("setImmediate"); }); Promise.resolve().then(() => { console.log("Promise.resolve"); }); process.nextTick(() => { console.log("process.nextTick"); }); console.log("end");
我们运行上面的代码,可以看到就算nextTick
定义在resolve
后面,它也是先执行的。
怎么理解这个穿插呢?其实就是在事件循环的六个阶段每个阶段执行完后会清空微任务队列。
我们来看例子,我们建立了timers、check、poll
三个阶段,并且每个阶段都产生了微任务。
// timers阶段 setTimeout(() => { console.log("setTimeout"); Promise.resolve().then(() => { console.log("setTimeout Promise.resolve"); }); }); // check阶段 setImmediate(() => { console.log("setImmediate"); Promise.resolve().then(() => { console.log("setImmediate Promise.resolve"); }); }); // 微任务 Promise.resolve().then(() => { console.log("Promise.resolve"); }); // 微任务 process.nextTick(() => { console.log("process.nextTick"); Promise.resolve().then(() => { console.log("nextTick Promise.resolve"); }); });
我们来执行上面的代码
可以看到,先执行微任务,再执行宏任务。先process.nextTick -> Promise.resolve
。并且如果微任务继续产生微任务则会再次清空,所以就又输出了nextTick Promise.resolve
。
接下来到timer
阶段,输出setTimeout
,并且产生了一个微任务,再进入到下个阶段前需要清空微任务队列,所以继续输出setTimeout Promise.resolve
。
接下来到check
阶段,输出setImmediate
,并且产生了一个微任务,再进入到下个阶段前需要清空微任务队列,所以继续输出setImmediate Promise.resolve
。
这也就印证了微任务会穿插在各个阶段之间运行。
所以对于Node
中的事件循环你只需要背好一以下几点就可以了
当主线程同步代码执行完毕后才会进入事件循环
事件循环总共分六个阶段,并且每个阶段都包括哪些回调需要记清楚。
事件循环中会先执行微任务再执行宏任务。
微任务会穿插在这六个阶段之间执行,每进入到下个阶段前会清空当前的微任务队列。
微任务中process.nextTick
的优先级最高,会优先执行。
更多node相关知识,请访问:nodejs 教程!
以上是深入了解Node事件循環(EventLoop)機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!