詳解NodeJS事件循環

官網:node官網-事件循環

瀏覽器中的事件循環是由HTML規范來定義,之后由各瀏覽器廠商實現的,而node中的事件循環的定義與實現均由libuv引擎完成。

node使用chrome v8引擎作為js解釋器,v8引擎分析代碼后,主線程立即執行同步任務,而異步任務則由libuv引擎驅動執行,而且不同異步任務的回調事件會放在不同的隊列中等待主線程執行,不再是簡單的宏任務隊列和微任務隊列。因此在nodeJS中,雖然程序運行表現出的整體狀態與瀏覽器中傳統的js大致相同,先同步后異步,但是對于異步的部分,node則依靠libuv引擎來進行更復雜的管理。

宏任務隊列和微任務隊列

六個基本階段(六個宏任務隊列)

在這里插入圖片描述

  1. timers:計時器階段,處理setTimeout()和setInterval()定時器的回調函數
  2. pending callbacks :待定回調階段,用于處理系統級別的錯誤信息,例如 TCP 錯誤或者 DNS 解析異常
  3. idle,prepare:僅在內部使用,可以忽略不計
  4. poll:輪詢階段,等待I/O事件(如網絡請求或者文件I/O等)的發生,然后執行對應的回調函數,并且會處理定時器相關的回調函數。如果沒有任何I/O事件發生,此階段可能會使事件循環阻塞
  5. check:檢查階段,處理 setImmediate() 的回調函數。check 的回調優先級比 setTimeout 高,比微任務要低
  6. close callbacks:關閉回調階段,處理一些關閉的回調函數,比如 socket.on(‘close’)

nextTick隊列(微任務隊列)

該事件隊列獨立于6個階段的事件隊列之外,用于存儲 process.nextTick() 的回調函數。

microTask隊列(微任務隊列)

該事件隊列也獨立于6個階段的事件隊列之外,用于存儲 Promise(Promise.then()、Promise.catch()、Promise.finally())的回調函數。

NodeJS事件循環流程

以上六個基本階段和兩個獨立的事件隊列構成了node事件循環的核心部分,在一次循環迭代的流程中,需要注意:

  1. nextTick隊列、microTask隊列中的任務穿插于6個階段之間進行,每個階段進行前會先執行并清空nextTick隊列、microTask隊列中的回調任務(可以理解為一次循環迭代至少處理6次nextTick隊列和microTask隊列中的任務)
  2. nextTick隊列、microTask隊列執行的次數在Node v11.x版本前后有一些差異,(上文中的至少很有深意),具體如下:
    a. Node版本小于11時,nextTick隊列、microTask隊列中的任務只會在6個階段之間進行,因此一次循環迭代最多處理6次這兩個隊列
    b. Node版本大于11時,任何一個階段的事件隊列中任務之間都會處理一次這兩個隊列,因此一次循環迭代至少處理6次這兩個隊列,上限則受各個階段總任務數影響而不固定
    c. 上述2個版本之間的區別,被認為是一個應該要修復的bug,因此在v11.x之后,node修改了nextTick隊列、microTask隊列的處理時機。從宏、微任務的角度看,修復后的流程和傳統js的事件循環保持了一致
  3. nextTick隊列中任務的優先級高于microTask隊列

setTimeout() 與 setlmmediate() 的特殊情況

我們知道 setTimeout()的回調是在 timers階段執行,setImmediate()的回調是在 check階段執行,并且事件循環是從 timers階段開始的,那么 setTimeout()的回調一定會先于 setImmediate()的回調執行嗎?答案是不一定。在只有這兩個函數且近乎同時觸發的情況下,它們回調的執行順序不是固定的(受調用時機、計算機性能影響)。下面是一個例子:

// 示例1(node v12.16.3)
setTimeout(() => {console.log("setTimeout");
});setImmediate(() => {console.log("setImmediate");
});// 結果:
// setTimeout -> setImmediate
// 或
// setImmediate -> setTimeout

上面示例1中的這段代碼輸出結果就是不固定的,這是因為這種情況下回調不一定完全準備好了。因為主線程沒有同步代碼需要執行,程序一開始就進入了事件循環。這時setTimeout()的回調并不是一定完全準備好了,因此就可能會在第一次循環迭代的check階段中執行setImmediate()的回調,再到第二次循環迭代的timers階段執行setTimeout()的回調;同時也有可能setTimeout()的回調一開始就準備好了,這樣就會按照先setTimeout()再setImmediate()的順序執行回調。由此就造成了輸出結果不固定的現象。

有以下兩種方法可以使輸出順序固定:
① 人為添加同步代碼的延時器,保證回調都準備好了(延時器的時長設定可能會受機器運行程序時的性能影響,因此該方法嚴格意義上并不能100%固定順序)。
② 將這兩個方法放入pending callbacks、idle,prepare、poll階段中任意一個階段即可,因為這些階段執行完后是一定會先到check再到下一個迭代的timers。由于pending callbacks、idle,prepare階段都偏向于系統內部,因此一般可以放入poll階段中使用。

如下示例2,我們人為加上一個2000ms的延時器,輸出的結果就固定了,如下所示:

//示例2(node v12.16.3)
setTimeout(() => {console.log("setTimeout");
});setImmediate(() => {console.log("setImmediate2");
});const sleep = (delay) => {const startTime = +new Date();while (+new Date() - startTime < delay) {continue;}
};
sleep(2000);// 結果:setTimeout -> setImmediate

如下示例3,我們將函數放入文件I/O的回調中,輸出的結果也就固定了,如下所示:

//示例3(node v12.16.3)
const fs = require("fs");fs.readFile("./fstest.js", "utf8", (err, data) => {setTimeout(() => {console.log("setTimeout");});setImmediate(() => {console.log("setImmediate");});
});// 結果:setImmediate -> setTimeout 

NodeJS事件循環示例

console.log('1'); //1層同步//1層timers,setTimeout1
setTimeout(function() {console.log('2'); //2層同步process.nextTick(function() {console.log('3'); //2層nextTick隊列})new Promise(function(resolve) {console.log('4'); //2層同步resolve();}).then(function() {console.log('5'); //2層microTask隊列})
})process.nextTick(function() {console.log('6'); //1層nextTick隊列
})new Promise(function(resolve) {console.log('7'); //1層同步resolve();
}).then(function() {console.log('8'); //1層microTask隊列
})//1層timers,setTimeout2
setTimeout(function() {console.log('9'); //2層同步process.nextTick(function() {console.log('10'); //2層nextTick隊列})new Promise(function(resolve) {console.log('11'); //2層同步resolve();}).then(function() {console.log('12'); //2層microTask隊列})
})console.log('13'); //1層同步//(node v12.16.3)結果:1 -> 7 -> 13 -> 6 -> 8 -> 2 -> 4 -> 3 -> 5 -> 9 -> 11 -> 10 -> 12
//(node v8.16.0)結果:1 -> 7 -> 13 -> 6 -> 8 -> 2 -> 4 -> 9 -> 11 -> 3 -> 10 -> 5 -> 12

圖解:node12+版本下的執行順序

在這里插入圖片描述

  1. 首先是1層的同步任務直接執行:1、7、13
  2. 進入事件循環
  3. 執行1層的nextTick隊列:6
  4. 執行1層的microTask隊列:8
  5. 進入timer階段,由于setTimeout1的回調任務先進入隊列,因此先執行setTimeout1的2層同步任務:2、4
  6. 執行setTimeout1的2層nextTick隊列:3
  7. 執行setTimeout1的2層microTask隊列:5
  8. setTimeout1的2層代碼均執行完畢,再執行setTimeout2的2層同步代碼:9、11
  9. 執行setTimeout2的2層nextTick隊列:10
  10. 執行setTimeout2的2層microTask隊列:12

和瀏覽器中事件循環的區別

瀏覽器事件循環在每次宏任務執行后,瀏覽器有機會進行UI渲染,但實際渲染取決于是否觸發了重排或重繪。
● 執行環境:瀏覽器的事件循環主要運行在JavaScript引擎和渲染引擎之間,而Node.js的事件循環是運行在單獨的線程中。這意味著在瀏覽器中,事件循環可能與渲染進程共享同一個線程,可能會出現線程阻塞的情況。而在Node.js中,事件循環運行在單獨的線程中,不會導致瀏覽器那樣的渲染阻塞
● 宏任務和微任務的實現方式:在瀏覽器中,宏任務和微任務是通過HTML5規范中定義的消息隊列來實現的。所有異步任務都被分為宏任務和微任務兩種類型,并依次加入到對應的隊列中。當當前的宏任務執行完畢后,會立即執行所有的微任務,然后再選擇下一個宏任務執行。常見的宏任務包括setTimeout、setInterval、DOM事件等,常見的微任務包括Promise.then、MutationObserver等
● 微任務隊列的執行時機:在瀏覽器事件循環中,每執行完一個宏任務后,便要檢查執行微任務隊列。而在Node事件循環中,微任務是在兩個階段之間執行的,即在"上一階段"執行完,"下一階段"開始前執行微任務隊列中的任務。這意味著Node中的微任務是在兩個階段之間執行的,而瀏覽器中的微任務是在每個宏任務執行完后執行的
● 事件循環的執行機制:瀏覽器的事件循環是在HTML5中定義的規范,而Node的事件循環則是由libuv庫實現。這兩個環境的事件循環執行機制不相同,不可以混為一談
● process.nextTick()的優先級:在Node.js中,process.nextTick()的優先級要高于其他微任務,也就是說,在兩個階段之間執行微任務時,若存在process.nextTick(),則先執行它,然后再執行其他微任務
● 事件循環的執行順序:瀏覽器的事件循環機制包括同步代碼的執行、宏任務隊列的執行、微任務隊列的執行以及瀏覽器UI線程的渲染工作。如果有Web Worker任務,也會被執行。而在Node.js中,事件循環的執行順序包括腳本作為宏任務的執行、微任務的執行以及可能的Web Worker任務的執行

應用場景影響

● Node.js 更強調后端服務的高效I/O處理和高并發能力,因此其事件循環機制側重于快速響應I/O事件和維持穩定的事件處理流。
● 瀏覽器 則側重于UI渲染和用戶交互的實時響應,故其事件循環設計確保了UI的流暢更新和事件的及時處理。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/12657.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/12657.shtml
英文地址,請注明出處:http://en.pswp.cn/web/12657.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

立創EDA繪制PCB電路板

1、繪制好原理圖后&#xff0c;點擊設計---原理圖轉PCB&#xff0c;生成PCB文件 2、將元器件拖入電路板方框內&#xff0c;擺放布局并使用工具欄布線、放置過孔及絲印 3、然后頂層和底層鋪銅 4、后面就可以生成制板文件發送嘉立創制板了。

Redis 本機無法訪問

問題 我在服務器上有兩個 Redis 實例&#xff0c;服務端口號分別是 6379 和 6380&#xff0c;Redis 服務器地址假設為 10.0.0.12。其中 6379 這個實例不需要密碼即可訪問&#xff0c;6380 需要密碼訪問。 在正常使用幾天后&#xff0c;本機突然無法訪問 6379 這個實例&#x…

springboot 定時任務解決方案

Scheduled (springboot 自帶的 注解) 基于注解Scheduled默認為單線程&#xff0c;開啟多個任務時&#xff0c;任務的執行時機會受上一個任務執行時間的影響。 EnableScheduling注解&#xff1a; 在配置類上使用&#xff0c;開啟計劃任務的支持&#xff08;類上&#xff09;。…

羊大師,羊奶真不錯

羊大師&#xff0c;羊奶真不錯 在眾多乳制品中&#xff0c;羊奶以其獨特的營養價值和美味口感贏得了人們的青睞。今天&#xff0c;小編羊大師想與大家分享羊奶的種種優點&#xff0c;讓我們一同領略它的魅力。 羊奶的營養價值極高。它含有豐富的蛋白質、脂肪、礦物質和維生素&…

無獨立顯卡如何安裝Pytorch

以前我是直接在colab中使用pytorch&#xff0c;非常方便&#xff0c;今天折騰了一上午&#xff0c;終于搞定了pytorh的安裝和環境設置&#xff0c;分享下我的安裝流程&#xff0c;遇到的問題和解決方案。 1. 用pip安裝Pytorch 打開cmd窗口&#xff08;按win R&#xff09;&a…

端午佳節,品嘗食家巷傳統面點與黃米粽子禮盒

端午佳節&#xff0c;品嘗食家巷傳統面點與黃米粽子禮盒 在這個端午節來臨之際&#xff0c;食家巷傾情推出一款別具特色的端午禮盒&#xff0c;將甘肅的傳統面點與地方特色黃米粽子完美融合&#xff0c;為您帶來一場美味與傳統的邂逅。 這款禮盒以甘肅傳統面點一窩絲、油餅和烤…

Android使用SQLite數據庫no such table 問題

sqlite找不到表的問題&#xff0c;大多出現在模擬器里&#xff0c;因為db文件沒找到&#xff0c;導致報錯&#xff0c;為了避免此問題&#xff0c;增加了數據庫文件是否存在的判斷&#xff0c;就可以完美解決此類問題。如果能幫到您&#xff0c;麻煩點個贊。 可以直接看這個方…

電子郵箱怎么注冊?電子郵箱注冊教程,只需要三步

電子郵箱在我們工作和生活中都是必不可少的溝通工具。電子郵箱怎么注冊&#xff1f;電子郵箱的注冊步驟是怎么樣的&#xff1f;本文將從電子郵箱注冊前的準備資料介紹&#xff0c;到具體電子郵箱注冊的三個步驟進行詳細講解。 一、電子郵箱注冊前的準備 電子郵箱是一個具有唯…

概率密度函數pdf的某種解釋與洞察

1.一個想法實驗 我在想一個數,姑且稱之為X,介于0和10之間(含0和10)。如果我不告訴你別的,你會想象X = 0的概率是多少?X = 4?假設我對任何特定的數字都沒有偏好,你會想象十一個整數0,1,2,.….,10也是一樣。因為所有的概率加起來必須是1,所以邏輯上的結論是給11個選項…

Python上下文管理器with塊及@contextmanager的用法

上下文管理器和with塊 上下文管理器對象存在的目的是為了管理with塊,就像迭代器的存在是為了管理for循環一樣 with 語句存在的意義是對一些常用的 try/finally 結構予以簡化。這種結構能夠保障一段代碼在運行完成后實施某項操作,就算該段代碼因為 return 語句、異常或者 sy…

聯豐策略炒股官網分析地產鏈條中的家電,一個不能再忽視的板塊

查查配“上漲放量,盤整縮量”是近期市場的一個重要特征,這說明空頭衰竭、新的做多力量或正在蓄力。昨天我們也以調查問卷的方式與大家進行了討論,對于市場未來將會如何演繹?近一半投票認為“牛在路上,逢低加倉”。與此同時,當前市場中,多條主線還在發力,比如地產鏈條中的家電,…

Python項目——基于回合制的RPG游戲設計與實現

基于回合制的RPG游戲設計與實現 項目概述 《魔法冒險》是一款基于回合制戰斗的角色扮演游戲。玩家將創建一個角色&#xff0c;探索世界&#xff0c;戰斗敵人&#xff0c;收集物品并提升等級。 項目設計報告 一、引言 本項目的目標是實現一個基于回合制戰斗的 RPG 游戲&…

買了個彩票,哈哈哈哈哈。

買了個彩票-雙色球&#xff0c;發現挺有意思的。 索性把雙色球的所有期的中獎號碼的數據都爬了下來&#xff0c;03至今&#xff0c;21年了。txt文本&#xff0c;6.5MB大小。 大家有啥好的建議&#xff0c;分析一下數據呢。

劉邦痛恨的叛徒雍齒,為何后來還被封了侯?

雍齒&#xff0c;原是沛縣的世族出身&#xff0c;家庭往上追溯幾代&#xff0c;也曾經顯赫過。 雖然比不上先祖世代為楚將的項梁、項羽&#xff0c;但雍齒這個沒落的世族后代&#xff0c;身上多多少少也還講究點貴族遺風。 戰國時期&#xff0c;以秦國的軍功爵制為代表&#…

亞馬遜等平臺有哪些風控因素,如何真正做好自養號測評

很多測評人都知道亞馬遜風控是非常嚴的&#xff0c;想要做好亞馬遜測評需要解決很多風控問題&#xff0c;但是往往很多測評工作室技術不夠&#xff0c;或者根本不了解風控點&#xff0c;以為只要IP或者指紋瀏覽器就可以做&#xff0c;這是非常錯誤的&#xff0c;也導致了很多隱…

SpringBoot(三)之打包方式

SpringBoot&#xff08;三&#xff09;之打包方式 Spring Boot 提供了幾種常見的打包方式&#xff0c;具體取決于你的項目需求和偏好&#xff1a; JAR&#xff08;可執行的JAR文件&#xff09;&#xff1a; 這是 Spring Boot 的默認打包方式。它將項目的所有依賴打包到一個可…

3D 生成重建009-DreamGaussian使用gaussian splatting在兩分鐘內生成3d

3D 生成重建009-DreamGaussian使用gaussian splatting在兩分鐘內生成3d 文章目錄 0 論文工作1 論文方法2 效果 0 論文工作 DreamGaussian是第一個使用gaussian splatting方法進行3d生成的工作。論文最先使用gaussian splatting替代原來用nerf表示3d。整體架構依然保留了原來的…

Jackson XML

Jackson XML 1 添加依賴2 XML轉對象3 對象轉XML4 根據路徑讀取 1 添加依賴 <dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId><version>2.11.3</version> </de…

在Spring的try-catch塊中手動實現事務回滾

在Spring應用開發中&#xff0c;Transactional注解為我們提供了強大的聲明式事務管理能力&#xff0c;使得我們能夠專注于業務邏輯而無需過多關注底層的事務處理細節。然而&#xff0c;在某些特定場景下&#xff0c;開發者可能需要在捕獲到特定異常時手動控制事務的回滾行為。本…