精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Nodejs進階 | 一文吃透異步I/O和事件循環(huán)

開發(fā) 前端
本文講詳細講解 nodejs 中兩個比較難以理解的部分異步I/O和事件循環(huán),對 nodejs 核心知識點,做梳理和補充。

[[420503]]

一 前言

本文講詳細講解 nodejs 中兩個比較難以理解的部分異步I/O和事件循環(huán),對 nodejs 核心知識點,做梳理和補充。

送人玫瑰,手有余香,希望閱讀后感覺不錯的同學,可以給點個贊,鼓勵我繼續(xù)創(chuàng)作前端硬文。

老規(guī)矩我們帶上疑問開始今天的分析??????:

  • 1 說說 nodejs 的異步I/O ?
  • 2 說說 nodejs 的事件循環(huán)機制 ?
  • 3 介紹一下 nodejs 中事件循環(huán)的各個階段 ?
  • 4 nodejs 中 promise 和 nextTick 的區(qū)別?
  • 5 nodejs 中 setImmediate 和 setTimeout 區(qū)別 ?
  • 6 setTimeout 是精確的嗎,什么情況影響 setTimeout 的執(zhí)行?
  • 7 nodejs 中事件循環(huán)和瀏覽器有什么不同 ?

二 異步I/O

概念

處理器訪問任何寄存器和 Cache 等封裝以外的數(shù)據(jù)資源都可以當成 I/O 操作,包括內(nèi)存,磁盤,顯卡等外部設備。在 Nodejs 中像開發(fā)者調(diào)用 fs 讀取本地文件或網(wǎng)絡請求等操作都屬于I/O操作。(最普遍抽象 I/O 是文件操作和 TCP/UDP 網(wǎng)絡操作)

Nodejs 為單線程的,在單線程模式下,任務都是順序執(zhí)行的,但是前面的任務如果用時過長,那么勢必會影響到后續(xù)任務的進行,通常 I/O 與 cpu 之間的計算是可以并行進行的,但是同步的模式下,I/O的進行會導致后續(xù)任務的等待,這樣阻塞了任務的執(zhí)行,也造成了資源不能很好的利用。

為了解決如上的問題,Nodejs 選擇了異步I/O的模式,讓單線程不再阻塞,更合理的使用資源。

如何合理的看待Nodejs中異步I/O

前端開發(fā)者可能更清晰瀏覽器環(huán)境下的 JS 的異步任務,比如發(fā)起一次 ajax 請求,正如 ajax 是瀏覽器提供給 js 執(zhí)行環(huán)境下可以調(diào)用的 api 一樣 ,在 Nodejs 中提供了 http 模塊可以讓 js 做相同的事。比如監(jiān)聽|發(fā)送 http 請求,除了 http 之外,nodejs 還有操作本地文件的 fs 文件系統(tǒng)等。

如上 fs http 這些任務在 nodejs 中叫做 I/O 任務。理解了 I/O 任務之后,來分析一下在 Nodejs 中,I/O 任務的兩種形態(tài)——阻塞和非阻塞。

nodejs中阻塞和非阻塞IO

nodejs 對于大部分的 I/O 操作都提供了阻塞和非阻塞兩種用法。阻塞指的是執(zhí)行 I/O 操作的時候必須等待結果,才往下執(zhí)行 js 代碼。如下一下阻塞代碼

阻塞I/O

  1. /* TODO:  阻塞 */ 
  2. const fs = require('fs'); 
  3. const data = fs.readFileSync('./file.js'); 
  4. console.log(data) 
  • 代碼阻塞 :讀取同級目錄下的 file.js 文件,結果 data 為 buffer 結構,這樣當讀取過程中,會阻塞代碼的執(zhí)行,所以 console.log(data) 將被阻塞,只有當結果返回的時候,才能正常打印 data 。
  • 異常處理 :如上操作有一個致命點就是,如果出現(xiàn)了異常,(比如在同級目錄下沒有 file.js 文件),就會讓整個程序報錯,接下來的代碼講不會執(zhí)行。通常需要 try catch來捕獲錯誤邊界。代碼如下:
  1. /* TODO: 阻塞 - 捕獲異常  */ 
  2. try{ 
  3.     const fs = require('fs'); 
  4.     const data = fs.readFileSync('./file1.js'); 
  5.     console.log(data) 
  6. }catch(e){ 
  7.     console.log('發(fā)生錯誤:',e) 
  8. console.log('正常執(zhí)行'

如上即便發(fā)生了錯誤,也不會影響到后續(xù)代碼的執(zhí)行以及應用程序發(fā)生錯誤導致的退出。

阻塞 I/O 造成代碼執(zhí)行等待 I/O 結果,浪費等待時間,CPU 的處理能力得不到充分利用,I/O 失敗還會讓整整個線程退出。阻塞 I / O 在整個調(diào)用棧上示意圖如下:

非阻塞I/O

Nodejs 的非阻塞 I/O 采用的是異步模式,就是剛剛介紹的異步I/O。首先看一下異步模式下的 I/O 操作:

  1. /* TODO: 非阻塞 - 異步 I/O */ 
  2. const fs = require('fs'
  3. fs.readFile('./file.js',(err,data)=>{ 
  4.     console.log(err,data) // null  <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 68 65 6c 6c 6f 2c 77 6f 72 6c 64 27 29> 
  5. }) 
  6. console.log(111) // 111 先被打印~ 
  7.  
  8. fs.readFile('./file1.js',(err,data)=>{ 
  9.     console.log(err,data) // 保存  [ no such file or directory, open './file1.js'] ,找不到文件。 
  10. }) 
  • 回調(diào) callback 被異步執(zhí)行,返回的第一個參數(shù)是錯誤信息,如果沒有錯誤,那么返回 null ,第二個參數(shù)為 fs.readFile 執(zhí)行得到的真正內(nèi)容。
  • 這種異步的形式可以會優(yōu)雅的捕獲到執(zhí)行 I/O 中出現(xiàn)的錯誤,比如說如上當讀取 file1.js 文件時候,出現(xiàn)了找不到對應文件的異常行為,會直接通過第一個參數(shù)形式傳遞到 callback 中。

比如如上的 callback ,作為一個異步回調(diào)函數(shù),就像 setTimeout(fn) 的 fn 一樣,不會阻塞代碼執(zhí)行。會在得到結果后觸發(fā),對于 Nodejs 異步執(zhí)行 I/O 回調(diào)的細節(jié),接下來會慢慢剖析。

對于異步 I/O 的處理, Nodejs 內(nèi)部使用了線程池來處理異步 I/O 任務,線程池中會有多個 I/O 線程來同時處理異步的 I/O 操作,比如如上的的例子中,在整個 I/O 模型中會這樣。

接下來將一起探索一下異步 I/O 執(zhí)行過程。

事件循環(huán)

和瀏覽器一樣,Nodejs 也有自身的執(zhí)行模型——事件循環(huán)( eventLoop ),事件循環(huán)的執(zhí)行模型受到宿主環(huán)境的影響,它不屬于 javascript 執(zhí)行引擎( 例如 v8 )的一部分,這就導致了不同宿主環(huán)境下事件循環(huán)模式和機制可能不同,直觀的體現(xiàn)就是 Nodejs 和瀏覽器環(huán)境下對微任務( microtask )和宏任務( macrotask )處理存在差異。對于 Nodejs 的事件循環(huán)及其每一個階段,接下來會詳細探討。

Nodejs 的事件循環(huán)有多個階段,其中有一個專門處理 I/O 回調(diào)的階段,每一個執(zhí)行階段我們可以稱之為 Tick , 每一個 Tick 都會查詢是否還有事件以及關聯(lián)的回調(diào)函數(shù) ,如上異步 I/O 的回調(diào)函數(shù),會在 I/O 處理階段檢查當前 I/O 是否完成,如果完成,那么執(zhí)行對應的 I/O 回調(diào)函數(shù),那么這個檢查 I/O 是否完成的觀察者我們稱之為 I/O 觀察者。

觀察者

如上提到了 I/O 觀察者的概念,也講了 Nodejs 中會有多個階段,事實上每一個階段都有一個或者多個對應的觀察者,它們的工作很明確就是在每一次對應的 Tick 過程中,對應的觀察者查找有沒有對應的事件執(zhí)行,如果有,那么取出來執(zhí)行。

瀏覽器的事件來源于用戶的交互和一些網(wǎng)絡請求比如 ajax 等, Nodejs 中,事件來源于網(wǎng)絡請求 http ,文件 I/O 等,這些事件都有對應的觀察者,我這里枚舉出一些重要的觀察者。

  • 文件 I/O 操作 —— I/O 觀察者;
  • 網(wǎng)絡 I/O 操作 —— 網(wǎng)絡 I/O 觀察者;
  • process.nextTick —— idle 觀察者
  • setImmediate —— check 觀察者
  • setTimeout/setInterval —— 延時器觀察者
  • ...

在 Nodejs 中,對應觀察者接收對應類型的事件,事件循環(huán)過程中,會向這些觀察者詢問有沒有該執(zhí)行的任務,如果有,那么觀察者會取出任務,交給事件循環(huán)去執(zhí)行。

請求對象與線程池

從 JavaScript 調(diào)用到計算機系統(tǒng)執(zhí)行完 I/O 回調(diào),請求對象充當著很重要的作用,我們還是以一次異步 I/O 操作為例

請求對象: 比如之前調(diào)用 fs.readFile ,本質(zhì)上調(diào)用 libuv 上的方法創(chuàng)建一個請求對象。這個請求對象上保留著此次 I/O 請求的信息,包括此次 I/O 的主體和回調(diào)函數(shù)等。然后異步調(diào)用的第一階段就完成了,JavaScript 會繼續(xù)往下執(zhí)行執(zhí)行棧上的代碼邏輯,當前的 I/O 操作將以請求對象的形式放入到線程池中,等待執(zhí)行。達到了異步 I/O 的目的。

線程池: Nodejs 的線程池在 Windows 下有內(nèi)核( IOCP )提供,在 Unix 系統(tǒng)中由 libuv 自行實現(xiàn), 線程池用來執(zhí)行部分的 I/O (系統(tǒng)文件的操作),線程池大小默認為 4 ,多個文件系統(tǒng)操作的請求可能阻塞到一個線程中。那么線程池里面的 I/O 操作是怎么執(zhí)行的呢?上一步說到,一次異步 I/O 會把請求對象放在線程池中,首先會判斷當前線程池是否有可用的線程,如果線程可用,那么會執(zhí)行請求對象的 I/O 操作,并把執(zhí)行后的結果返回給請求對象。在事件循環(huán)中的 I/O 處理階段,I/O 觀察者會獲取到已經(jīng)完成的 I/O 對象,然后取出回調(diào)函數(shù)和結果調(diào)用執(zhí)行。I/O 回調(diào)函數(shù)就這樣執(zhí)行,而且在回調(diào)函數(shù)的參數(shù)重獲取到結果。

異步 I/O 操作機制

上述講了整個異步 I/O 的執(zhí)行流程,從一個異步 I/O 的觸發(fā),到 I/O 回調(diào)到執(zhí)行。事件循環(huán) ,觀察者 ,請求對象 ,線程池 構成了整個異步 I/O 執(zhí)行模型。

用一幅圖表示四者的關系:

總結上述過程:

  • 第一階段:每一次異步 I/O 的調(diào)用,首先在 nodejs 底層設置請求參數(shù)和回調(diào)函 callback,形成請求對象。
  • 第二階段:形成的請求對象,會被放入線程池,如果線程池有空閑的 I/O 線程,會執(zhí)行此次 I/O 任務,得到結果。
  • 第三階段:事件循環(huán)中 I/O 觀察者,會從請求對象中找到已經(jīng)得到結果的 I/O 請求對象,取出結果和回調(diào)函數(shù),將回調(diào)函數(shù)放入事件循環(huán)中,執(zhí)行回調(diào),完成整個異步 I/O 任務。

對于如何感知異步 I/O 任務執(zhí)行完畢的?以及如何獲取完成的任務的呢?libuv 作為中間層, 在不同平臺上,采用手段不同,在 unix 下通過 epoll 輪詢,在 Windows 下通過內(nèi)核( IOCP )來實現(xiàn) ,F(xiàn)reeBSD 下通過 kqueue 實現(xiàn)。

三 事件循環(huán)

事件循環(huán)機制由宿主環(huán)境實現(xiàn)

上述中已經(jīng)提及了事件循環(huán)不是 JavaScript 引擎的一部分 ,事件循環(huán)機制由宿主環(huán)境實現(xiàn),所以不同宿主環(huán)境下事件循環(huán)不同 ,不同宿主環(huán)境指的是瀏覽器環(huán)境還是 nodejs 環(huán)境 ,但在不同操作系統(tǒng)中,nodejs 的宿主環(huán)境也是不同的,接下來用一幅圖描述一下 Nodejs 中的事件循環(huán)和 javascript 引擎之間的關系。

以 libuv 下 nodejs 的事件循環(huán)為參考,關系如下:

以瀏覽器下 javaScript 的事件循環(huán)為參考,關系如下:

事件循環(huán)本質(zhì)上就像一個 while 循環(huán),如下所示,我來用一段代碼模擬事件循環(huán)的執(zhí)行流程。

  1. const queue = [ ... ]   // queue 里面放著待處理事件 
  2. while(true){ 
  3.     //開始循環(huán) 
  4.     //執(zhí)行 queue 中的任務 
  5.     //.... 
  6.  
  7.     if(queue.length ===0){ 
  8.        return // 退出進程 
  9.     } 
  • Nodejs 啟動后,就像創(chuàng)建一個 while 循環(huán)一樣,queue 里面放著待處理的事件,每一次循環(huán)過程中,如果還有事件,那么取出事件,執(zhí)行事件,如果存在事件關聯(lián)的回調(diào)函數(shù),那么執(zhí)行回調(diào)函數(shù),然后開始下一次循環(huán)。
  • 如果循環(huán)體中沒有事件,那么將退出進程。

我總結了流程圖如下所示:

那么如何事件循環(huán)是如何處理這些任務的呢?我們列出 Nodejs 中一些常用的事件任務:

  • setTimeout 或 setInterval 延時器計時器。
  • 異步 I/O 任務:文件任務 ,網(wǎng)絡請求等。
  • setImmediate 任務。
  • process.nextTick 任務。
  • Promise 微任務。

接下來會一一講到 ,這些任務的原理以及 nodejs 是如何處理這些任務的。

1 事件循環(huán)階段

對于不同的事件任務,會在不同的事件循環(huán)階段執(zhí)行。根據(jù) nodejs 官方文檔,在通常情況下,nodejs 中的事件循環(huán)根據(jù)不同的操作系統(tǒng)可能存在特殊的階段,但總體是可以分為以下 6 個階段 (代碼塊的六個階段) :

  1. /* 
  2.    ┌───────────────────────────┐ 
  3. ┌─>│           timers          │     -> 定時器,延時器的執(zhí)行     
  4. │  └─────────────┬─────────────┘ 
  5. │  ┌─────────────┴─────────────┐ 
  6. │  │     pending callbacks     │     -> i/o 
  7. │  └─────────────┬─────────────┘ 
  8. │  ┌─────────────┴─────────────┐ 
  9. │  │       idle, prepare       │ 
  10. │  └─────────────┬─────────────┘      ┌───────────────┐ 
  11. │  ┌─────────────┴─────────────┐      │   incoming:   │ 
  12. │  │           poll            │<─────┤  connections, │ 
  13. │  └─────────────┬─────────────┘      │   data, etc.  │ 
  14. │  ┌─────────────┴─────────────┐      └───────────────┘ 
  15. │  │           check           │ 
  16. │  └─────────────┬─────────────┘ 
  17. │  ┌─────────────┴─────────────┐ 
  18. └──┤      close callbacks      │ 
  19.    └───────────────────────────┘ 
  20. */ 
  • 第一階段:timer ,timer 階段主要做的事是,執(zhí)行 setTimeout 或 setInterval 注冊的回調(diào)函數(shù)。
  • 第二階段:pending callback ,大部分 I/O 回調(diào)任務都是在 poll 階段執(zhí)行的,但是也會存在一些上一次事件循環(huán)遺留的被延時的 I/O 回調(diào)函數(shù),那么此階段就是為了調(diào)用之前事件循環(huán)延遲執(zhí)行的 I/O 回調(diào)函數(shù)。
  • 第三階段:idle prepare 階段,僅用于 nodejs 內(nèi)部模塊的使用。
  • 第四階段:poll 輪詢階段,這個階段主要做兩件事,一這個階段會執(zhí)行異步 I/O 的回調(diào)函數(shù);二 計算當前輪詢階段阻塞后續(xù)階段的時間。
  • 第五階段:check階段,當 poll 階段回調(diào)函數(shù)隊列為空的時候,開始進入 check 階段,主要執(zhí)行 setImmediate 回調(diào)函數(shù)。
  • 第六階段:close階段,執(zhí)行注冊 close 事件的回調(diào)函數(shù)。

對于每一個階段的執(zhí)行特點和對應的事件任務,我接下來會詳細剖析。我們看一下六個階段在底層源碼中是怎么樣體現(xiàn)的。

我們看一下 libuv 下 nodejs 的事件循環(huán)的源代碼(在 unix 和 win 有點差別,不過不影響流程,這里以 unix 為例子。):

libuv/src/unix/core.c

  1. int uv_run(uv_loop_t* loop, uv_run_mode mode) { 
  2.   // 省去之前的流程。 
  3.   while (r != 0 && loop->stop_flag == 0) { 
  4.  
  5.     /* 更新事件循環(huán)的時間 */  
  6.     uv__update_time(loop); 
  7.  
  8.     /*第一階段:timer 階段執(zhí)行  */ 
  9.     uv__run_timers(loop); 
  10.  
  11.     /*第二階段:pending 階段 */ 
  12.     ran_pending = uv__run_pending(loop); 
  13.  
  14.     /*第三階段:idle prepare 階段 */ 
  15.     uv__run_idle(loop); 
  16.     uv__run_prepare(loop); 
  17.  
  18.     timeout = 0; 
  19.     if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) 
  20.      /* 計算 timeout 時間  */ 
  21.       timeout = uv_backend_timeout(loop); 
  22.      
  23.     /* 第四階段:poll 階段 */ 
  24.     uv__io_poll(loop, timeout); 
  25.  
  26.     /* 第五階段:check 階段 */ 
  27.     uv__run_check(loop); 
  28.     /* 第六階段:close 階段  */ 
  29.     uv__run_closing_handles(loop); 
  30.     /* 判斷當前線程還有任務 */  
  31.      r = uv__loop_alive(loop); 
  32.  
  33.     /* 省去之后的流程 */ 
  34.   } 
  35.   return r; 

我們看到六個階段是按序執(zhí)行的,只有完成上一階段的任務,才能進行下一階段

當 uv__loop_alive 判斷當前事件循環(huán)沒有任務,那么退出線程。

2 任務隊列

在整個事件循環(huán)過程中,有四個隊列(實際的數(shù)據(jù)結構不是隊列)是在 libuv 的事件循環(huán)中進行的,還有兩個隊列是在 nodejs 中執(zhí)行的分別是 promise 隊列 和 nextTick 隊列。

在 NodeJS 中不止一個隊列,不同類型的事件在它們自己的隊列中入隊。在處理完一個階段后,移向下一個階段之前,事件循環(huán)將會處理兩個中間隊列,直到兩個中間隊列為空。

libuv 處理任務隊列

事件循環(huán)的每一個階段,都會執(zhí)行對應任務隊列里面的內(nèi)容。

  • timer 隊列( PriorityQueue ):本質(zhì)上的數(shù)據(jù)結構是二叉最小堆,二叉最小堆的根節(jié)點獲取最近的時間線上的 timer 對應的回調(diào)函數(shù)。
  • I/O 事件隊列:存放 I/O 任務。
  • Immediate 隊列( ImmediateList ):多個 Immediate ,node 層用鏈表數(shù)據(jù)結構儲存。
  • 關閉回調(diào)事件隊列:放置待 close 的回調(diào)函數(shù)。

非 libuv 中間隊列

  • nextTick 隊列 :存放 nextTick 的回調(diào)函數(shù)。這個是在 nodejs 中特有的。
  • Microtasks 微隊列 Promise :存放 promise 的回調(diào)函數(shù)。

中間隊列的執(zhí)行特點:

  • 首先要明白兩個中間隊列并非在 libuv 中被執(zhí)行,它們都是在 nodejs 層執(zhí)行的,在 libuv 層處理每一個階段的任務之后,會和 node 層進行通訊,那么會優(yōu)先處理兩個隊列中的任務。
  • nextTick 任務的優(yōu)先級要大于 Microtasks 任務中的 Promise 回調(diào)。也就是說 node 會首先清空 nextTick 中的任務,然后才是 Promise 中的任務。為了驗證這個結論,例舉一個打印結果的題目如下:
  1. /* TODO: 打印順序  */ 
  2. setTimeout(()=>{ 
  3.     console.log('setTimeout 執(zhí)行'
  4. },0) 
  5.  
  6. const p = new Promise((resolve)=>{ 
  7.      console.log('Promise執(zhí)行'
  8.      resolve() 
  9. }) 
  10. p.then(()=>{ 
  11.     console.log('Promise 回調(diào)執(zhí)行'
  12. }) 
  13.  
  14. process.nextTick(()=>{ 
  15.     console.log('nextTick 執(zhí)行'
  16. }) 
  17. console.log('代碼執(zhí)行完畢'

如上代碼塊中的 nodejs 中的執(zhí)行順序是什么?

效果:

打印結果:Promise執(zhí)行 -> 代碼執(zhí)行完畢 -> nextTick 執(zhí)行 -> Promise 回調(diào)執(zhí)行 -> setTimeout 執(zhí)行

解釋:很好理解為什么這么打印,在主代碼事件循環(huán)中,Promise執(zhí)行 和 代碼執(zhí)行完畢 最先被打印,nextTick 被放入 nextTick 隊列中,Promise 回調(diào)放入 Microtasks 隊列中,setTimeout 被放入 timer 堆中。接下來主循環(huán)完成,開始清空兩個隊列中的內(nèi)容,首先清空 nextTick 隊列,nextTick 執(zhí)行 被打印,接下來清空 Microtasks 隊列,Promise 回調(diào)執(zhí)行 被打印,最后再判斷事件循環(huán) loop 中還有 timer 任務,那么開啟新的事件循環(huán) ,首先執(zhí)行,timer 任務,setTimeout 執(zhí)行被打印。整個流程完畢。

  • 無論是 nextTick 的任務,還是 promise 中的任務, 兩個任務中的代碼會阻塞事件循環(huán)的有序進行,導致 I/O 餓死的情況發(fā)生,所以需要謹慎處理兩個任務中的邏輯。比如如下:
  1. /* TODO: 阻塞 I/O 情況 */ 
  2. process.nextTick(()=>{ 
  3.     const now = +new Date() 
  4.     /* 阻塞代碼三秒鐘 */ 
  5.     while( +new Date() < now + 3000 ){} 
  6. }) 
  7.  
  8. fs.readFile('./file.js',()=>{ 
  9.     console.log('I/O: file '
  10. }) 
  11.  
  12. setTimeout(() => { 
  13.     console.log('setTimeout: '
  14. }, 0); 

效果:

三秒鐘, 事件循環(huán)中的 timer 任務和 I/O 任務,才被有序執(zhí)行。也就是說 nextTick 中的代碼,阻塞了事件循環(huán)的有序進行。

3 事件循環(huán)流程圖

接下來用流程圖,表示事件循環(huán)的六大階段的執(zhí)行順序,以及兩個優(yōu)先隊列的執(zhí)行邏輯。

4 timer 階段 -> 計時器 timer / 延時器 interval

延時器計時器觀察者(Expired timers and intervals):延時器計時器觀察者用來檢查通過 setTimeout 或 setInterval創(chuàng)建的異步任務,內(nèi)部原理和異步 I/O 相似,不過定期器/延時器內(nèi)部實現(xiàn)沒有用線程池。通過setTimeout 或 setInterval定時器對象會被插入到延時器計時器觀察者內(nèi)部的二叉最小堆中,每次事件循環(huán)過程中,會從二叉最小堆頂部取出計時器對象,判斷 timer/interval 是否過期,如果有,然后調(diào)用它,出隊。再檢查當前隊列的第一個,直到?jīng)]有過期的,移到下一個階段。

libuv 層如何處理 timer

首先一起看一下 libuv 層是如何處理的 timer

libuv/src/timer.c

  1. void uv__run_timers(uv_loop_t* loop) { 
  2.   struct heap_node* heap_node; 
  3.   uv_timer_t* handle; 
  4.  
  5.   for (;;) { 
  6.     /* 找到 loop 中 timer_heap 中的根節(jié)點 ( 值最小 ) */   
  7.     heap_node = heap_min((struct heap*) &loop->timer_heap); 
  8.     /*  */ 
  9.     if (heap_node == NULL
  10.       break; 
  11.  
  12.     handle = container_of(heap_node, uv_timer_t, heap_node); 
  13.     if (handle->timeout > loop->time
  14.       /*  執(zhí)行時間大于事件循環(huán)事件,那么不需要在此次 loop 中執(zhí)行  */ 
  15.       break; 
  16.  
  17.     uv_timer_stop(handle); 
  18.     uv_timer_again(handle); 
  19.     handle->timer_cb(handle); 
  20.   } 
  • 如上 handle timeout 可以理解成過期時間,也就是計時器回到函數(shù)的執(zhí)行時間。
  • 當 timeout 大于當前事件循環(huán)的開始時間時,即表示還沒有到執(zhí)行時機,回調(diào)函數(shù)還不應該被執(zhí)行。那么根據(jù)二叉最小堆的性質(zhì),父節(jié)點始終比子節(jié)點小,那么根節(jié)點的時間節(jié)點都不滿足執(zhí)行時機的話,其他的 timer 也不滿足執(zhí)行時間。此時,退出 timer 階段的回調(diào)函數(shù)執(zhí)行,直接進入事件循環(huán)下一階段。
  • 當過期時間小于當前事件循環(huán) tick 的開始時間時,表示至少存在一個過期的計時器,那么循環(huán)迭代計時器最小堆的根節(jié)點,并調(diào)用該計時器所對應的回調(diào)函數(shù)。每次循環(huán)迭代時都會更新最小堆的根節(jié)點為最近時間節(jié)點的計時器。

如上是 timer 階段在 libuv 中執(zhí)行特點。接下里分析一下 node 中是如何處理定時器延時器的。

node 層如何處理 timer

在 Nodejs 中 setTimeout 和 setInterval 是 nodejs 自己實現(xiàn)的,來一起看一下實現(xiàn)細節(jié):

node/lib/timers.js

  1. function setTimeout(callback,after){ 
  2.     //... 
  3.     /* 判斷參數(shù)邏輯 */ 
  4.     //.. 
  5.     /* 創(chuàng)建一個 timer 觀察者 */ 
  6.     const timeout = new Timeout(callback, after, args, falsetrue); 
  7.     /* 將 timer 觀察者插入到 timer 堆中  */ 
  8.     insert(timeout, timeout._idleTimeout); 
  9.  
  10.     return timeout; 

setTimeout:邏輯很簡單,就是創(chuàng)建一個 timer 時間觀察者,然后放入計時器堆中。

那么 Timeout 做了些什么呢?

node/lib/internal/timers.js

  1. function Timeout(callback, after, args, isRepeat, isRefed) { 
  2.   after *= 1  
  3.   if (!(after >= 1 && after <= 2 ** 31 - 1)) { 
  4.     after = 1 // 如果延時器 timeout 為 0 ,或者是大于 2 ** 31 - 1 ,那么設置成 1  
  5.   } 
  6.   this._idleTimeout = after; // 延時時間  
  7.   this._idlePrev = this; 
  8.   this._idleNext = this; 
  9.   this._idleStart = null
  10.   this._onTimeout = null
  11.   this._onTimeout = callback; // 回調(diào)函數(shù) 
  12.   this._timerArgs = args; 
  13.   this._repeat = isRepeat ? after : null
  14.   this._destroyed = false;   
  15.  
  16.   initAsyncResource(this, 'Timeout'); 

在 nodejs 中無論 setTimeout 還是 setInterval 本質(zhì)上都是 Timeout 類。超出最大時間閥 2 ** 31 - 1 或者 setTimeout(callback, 0) ,_idleTimeout 會被設置成 1 ,轉(zhuǎn)換為 setTimeout(callback, 1) 來執(zhí)行。

timer 處理流程圖

用一副流程圖描述一下,我們創(chuàng)建一個 timer ,再到 timer 在事件循環(huán)里面執(zhí)行的流程。

timer 特性

這里有兩點需要注意:

  • 執(zhí)行機制 :延時器計時器觀察者,每一次都會執(zhí)行一個,執(zhí)行一個之后會清空 nextTick 和 Promise, 過期時間是決定兩者是否執(zhí)行的重要因素,還有一點 poll 會計算阻塞 timer 執(zhí)行的時間,對 timer 階段任務的執(zhí)行也有很重要的影響。

驗證結論一次執(zhí)行一個 timer 任務 ,先來看一段代碼片段:

  1. setTimeout(()=>{ 
  2.     console.log('setTimeout1:'
  3.     process.nextTick(()=>{ 
  4.         console.log('nextTick'
  5.     }) 
  6. },0) 
  7. setTimeout(()=>{ 
  8.     console.log('setTimeout2:'
  9. },0) 

打印結果:

nextTick 隊列是在事件循環(huán)的每一階段結束執(zhí)行的,兩個延時器的閥值都是 0 ,如果在 timer 階段一次性執(zhí)行完,過期任務的話,那么打印 setTimeout1 -> setTimeout2 -> nextTick ,實際上先執(zhí)行一個 timer 任務,然后執(zhí)行 nextTick 任務,最后再執(zhí)行下一個 timer 任務。

  • 精度問題 :關于 setTimeout 的計數(shù)器問題,計時器并非精確的,盡管在 nodejs 的事件循環(huán)非常的快,但是從延時器 timeout 類的創(chuàng)建,會占用一些事件,再到上下文執(zhí)行, I/O 的執(zhí)行,nextTick 隊列執(zhí)行,Microtasks 執(zhí)行,都會阻塞延時器的執(zhí)行。甚至在檢查 timer 過期的時候,也會消耗一些 cpu 時間。
  • 性能問題 :如果想用 setTimeout(fn,0) 來執(zhí)行一些非立即調(diào)用的任務,那么性能上不如 process.nextTick 實在,首先 setTimeout 精度不夠,還有一點就是里面有定時器對象,并需要在 libuv 底層執(zhí)行,占用一定性能,所以可以用 process.nextTick 解決這種場景。

5 pending 階段

pending 階段用來處理此次事件循環(huán)之前延時的 I/O 回調(diào)函數(shù)。首先看一下在 libuv 中執(zhí)行時機。

libuv/src/unix/core.c

  1. static int uv__run_pending(uv_loop_t* loop) { 
  2.   QUEUE* q; 
  3.   QUEUE pq; 
  4.   uv__io_t* w 
  5.   /* pending_queue 為空,清空隊列 ,返回 0  */ 
  6.   if (QUEUE_EMPTY(&loop->pending_queue)) 
  7.     return 0; 
  8.    
  9.   QUEUE_MOVE(&loop->pending_queue, &pq); 
  10.   while (!QUEUE_EMPTY(&pq)) { /* pending_queue 不為空的情況,清空 I/O 回調(diào)。返回 1  */ 
  11.     q = QUEUE_HEAD(&pq); 
  12.     QUEUE_REMOVE(q); 
  13.     QUEUE_INIT(q); 
  14.     w = QUEUE_DATA(q, uv__io_t, pending_queue); 
  15.     w->cb(loop, w, POLLOUT); 
  16.   } 
  17.   return 1; 
  • 如果存放 I/O 回調(diào)的任務的 pending_queue 是空的,那么直接返回 0。
  • 如果 pending_queue 有 I/O 回調(diào)任務,那么執(zhí)行回調(diào)任務。

6 idle, prepare 階段

idle 做一些 libuv 一些內(nèi)部操作, prepare 為接下來的 I/O 輪詢做一些準備工作。接下來一起解析一下比較重要 poll 階段。

7 poll I / O 輪詢階段

在正式講解 poll 階段做哪些事情之前,首先看一下,在 libuv 中,輪詢階段的執(zhí)行邏輯:

  1. timeout = 0; 
  2.   if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) 
  3.     /* 計算 timeout   */ 
  4.     timeout = uv_backend_timeout(loop); 
  5.     /* 進入 I/O 輪詢 */ 
  6.     uv__io_poll(loop, timeout); 

初始化超時時間 timeout = 0 ,通過 uv_backend_timeout 計算本次 poll 階段的超時時間。超時時間會影響到異步 I/O 和后續(xù)事件循環(huán)的執(zhí)行。

timeout代表什么

首先要明白不同 timeout ,在 I/O 輪詢中代表什么意思。

  • 當 timeout = 0 的時候,說明 poll 階段不會阻塞事件循環(huán)的進行,那么說明有更迫切執(zhí)行的任務。那么當前的 poll 階段不會發(fā)生阻塞,會盡快進入下一階段,盡快結束當前 tick,進入下一次事件循環(huán),那么這些緊急任務將被執(zhí)行。
  • 當 timeout = -1時,說明會一直阻塞事件循環(huán),那么此時就可以停留在異步 I/O 的 poll 階段,等待新的 I/O 任務完成。
  • 當 timeout等于常數(shù)的情況,說明此時 io poll 循環(huán)階段能夠停留的時間,那么什么時候會存在 timeout 為常數(shù)呢,將馬上揭曉。

獲取timeout

timeout 的獲取是通過 uv_backend_timeout 那么如何獲得的呢?

  1. int uv_backend_timeout(const uv_loop_t* loop) { 
  2.     /* 當前事件循環(huán)任務停止 ,不阻塞 */ 
  3.   if (loop->stop_flag != 0) 
  4.     return 0; 
  5.    /* 當前事件循環(huán) loop 不活躍的時候 ,不阻塞 */ 
  6.   if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) 
  7.     return 0; 
  8.   /* 當 idle 句柄隊列不為空時,返回 0,即不阻塞。 */ 
  9.   if (!QUEUE_EMPTY(&loop->idle_handles)) 
  10.     return 0; 
  11.    /* i/o pending 隊列不為空的時候。 */   
  12.   if (!QUEUE_EMPTY(&loop->pending_queue)) 
  13.     return 0; 
  14.    /* 有關閉回調(diào) */ 
  15.   if (loop->closing_handles) 
  16.     return 0; 
  17.   /* 計算有沒有延時最小的延時器 | 定時器 */ 
  18.   return uv__next_timeout(loop); 

uv_backend_timeout 主要做的事情是:

  • 當前事件循環(huán)停止時,不阻塞。
  • 當前事件循環(huán) loop 不活躍的時候 ,不阻塞。
  • 當 idle 隊列 ( setImmediate ) 不為空時,返回 0,不阻塞。
  • i/o pending 隊列不為空的時候,不阻塞。
  • 有關閉回調(diào)函數(shù)的時候,不阻塞。
  • 如果上述均不滿足,那么通過 uv__next_timeout 計算有沒有延時閥值最小的定時器 | 延時器( 最急迫執(zhí)行 ),返回延時時間。

接下來看一下 uv__next_timeout 邏輯。

  1. int uv__next_timeout(const uv_loop_t* loop) { 
  2.   const struct heap_node* heap_node; 
  3.   const uv_timer_t* handle; 
  4.   uint64_t diff; 
  5.   /* 找到延時時間最小的 timer  */ 
  6.   heap_node = heap_min((const struct heap*) &loop->timer_heap); 
  7.   if (heap_node == NULL) /* 如何沒有 timer,那么返回 -1 ,一直進入 poll 狀態(tài)  */ 
  8.     return -1;  
  9.  
  10.   handle = container_of(heap_node, uv_timer_t, heap_node); 
  11.    /* 有過期的 timer 任務,那么返回 0,poll 階段不阻塞 */ 
  12.   if (handle->timeout <= loop->time
  13.     return 0; 
  14.   /* 返回當前最小閥值的 timer 與 當前事件循環(huán)的事件相減,得出來的時間,可以證明 poll 可以停留多長時間 */  
  15.   diff = handle->timeout - loop->time
  16.   return (int) diff; 

uv__next_timeout 做的事情如下:

  • 找到時間閥值最小的 timer (最優(yōu)先執(zhí)行的),如何沒有 timer,那么返回 -1 。poll 階段將無限制阻塞。這樣的好處是一旦有 I/O 執(zhí)行完畢 ,I/O 回調(diào)函數(shù)會直接加入到 poll ,接下來就會執(zhí)行對應的回調(diào)函數(shù)。
  • 如果有 timer ,但是 timeout <= loop.time 證明已經(jīng)過期了,那么返回 0,poll 階段不阻塞,優(yōu)先執(zhí)行過期任務。
  • 如果沒有過期,返回當前最小閥值的 timer 與 當前事件循環(huán)的事件相減得值,即是可以證明 poll 可以停留多長時間。當停留完畢,證明有過期 timer ,那么進入到下一個 tick。

執(zhí)行io_poll

接下來就是 uv__io_poll 真正的執(zhí)行,里面有一個 epoll_wait 方法,根據(jù) timeout ,來輪詢有沒有 I/O 完成,有得話那么執(zhí)行 I/O 回調(diào)。這也是 unix 下異步I/O 實現(xiàn)的重要環(huán)節(jié)。

poll階段本質(zhì)

接下來總結一下 poll 階段的本質(zhì):

  • poll 階段就是通過 timeout 來判斷,是否阻塞事件循環(huán)。poll 也是一種輪詢,輪詢的是 i/o 任務,事件循環(huán)傾向于 poll 階段的持續(xù)進行,其目的就是更快的執(zhí)行 I/O 任務。如果沒有其他任務,那么將一直處于 poll 階段。
  • 如果有其他階段更緊急待執(zhí)行的任務,比如 timer ,close ,那么 poll 階段將不阻塞,會進行下一個 tick 階段。

poll 階段流程圖

我把整個 poll 階段做的事用流程圖表示,省去了一些細枝末節(jié)。

8 check 階段

如果 poll 階段進入 idle 狀態(tài)并且 setImmediate 函數(shù)存在回調(diào)函數(shù)時,那么 poll 階段將打破無限制的等待狀態(tài),并進入 check 階段執(zhí)行 check 階段的回調(diào)函數(shù)。

check 做的事就是處理 setImmediate 回調(diào)。,先來看一下 Nodejs 中是怎么定義的 setImmediate。

Nodejs 底層中的 setImmediate

setImmediate定義

node/lib/timer.js

  1. function setImmediate(callback, arg1, arg2, arg3) { 
  2.   validateCallback(callback); /* 校驗一下回調(diào)函數(shù) */ 
  3.    /* 創(chuàng)建一個 Immediate 類   */ 
  4.    return new Immediate(callback, args); 
  • 當調(diào)用 setImmediate 本質(zhì)上調(diào)用 nodejs 中的 setImmediate 方法,首先校驗回調(diào)函數(shù),然后創(chuàng)建一個 Immediate 類。接下來看一下 Immediate 類。
  • node/lib/internal/timers.js
  1. class Immediate{ 
  2.    constructor(callback, args) { 
  3.     this._idleNext = null
  4.     this._idlePrev = null; /* 初始化參數(shù) */ 
  5.     this._onImmediate = callback; 
  6.     this._argv = args; 
  7.     this._destroyed = false
  8.     this[kRefed] = false
  9.  
  10.     initAsyncResource(this, 'Immediate'); 
  11.     this.ref(); 
  12.     immediateInfo[kCount]++; 
  13.      
  14.     immediateQueue.append(this); /* 添加 */ 
  15.   } 
  • Immediate 類會初始化一些參數(shù),然后將當前 Immediate 類,插入到 immediateQueue 鏈表中。
  • immediateQueue 本質(zhì)上是一個鏈表,存放每一個 Immediate。

setImmediate執(zhí)行

poll 階段之后,會馬上到 check 階段,執(zhí)行 immediateQueue 里面的 Immediate。在每一次事件循環(huán)中,會先執(zhí)行一個setImmediate 回調(diào),然后清空 nextTick 和 Promise 隊列的內(nèi)容。為了驗證這個結論,同樣和 setTimeout 一樣,看一下如下代碼塊:

  1. setImmediate(()=>{ 
  2.     console.log('setImmediate1'
  3.     process.nextTick(()=>{ 
  4.         console.log('nextTick'
  5.     }) 
  6. }) 
  7.  
  8. setImmediate(()=>{ 
  9.     console.log('setImmediate2'
  10. }) 

打印 setImmediate1 -> nextTick -> setImmediate2 ,在每一次事件循環(huán)中,執(zhí)行一個 setImmediate ,然后執(zhí)行清空 nextTick 隊列,在下一次事件循環(huán)中,執(zhí)行另外一個 setImmediate2 。

setImmediate執(zhí)行流程圖

setTimeout & setImmediate

接下來對比一下 setTimeout 和 setImmediate,如果開發(fā)者期望延時執(zhí)行的異步任務,那么接下來對比一下 setTimeout(fn,0) 和 setImmediate(fn) 區(qū)別。

  • setTimeout 是 用于在設定閥值的最小誤差內(nèi),執(zhí)行回調(diào)函數(shù),setTimeout 存在精度問題,創(chuàng)建 setTimeout 和 poll 階段都可能影響到 setTimeout 回調(diào)函數(shù)的執(zhí)行。
  • setImmediate 在 poll 階段之后,會馬上進入 check 階段,會執(zhí)行 setImmediate回調(diào)。

如果 setTimeout 和 setImmediate 在一起,那么誰先執(zhí)行呢?

首先寫一個 demo:

  1. setTimeout(()=>{ 
  2.     console.log('setTimeout'
  3. },0) 
  4.  
  5. setImmediate(()=>{ 
  6.     console.log( 'setImmediate' ) 
  7. }) 

猜測

先猜測一下,setTimeout 發(fā)生 timer 階段,setImmediate 發(fā)生在 check 階段,timer 階段早于 check 階段,那么 setTimeout 優(yōu)先于 setImmediate 打印。但事實是這樣嗎?

實際打印結果

從以上打印結果上看, setTimeout 和 setImmediate 執(zhí)行時機是不確定的,為什么會造成這種情況,上文中講到即使 setTimeout 第二個參數(shù)為 0,在 nodejs 中也會被處理 setTimeout(fn,1)。當主進程的同步代碼執(zhí)行之后,會進入到事件循環(huán)階段,第一次進入 timer 中,此時 settimeout 對應的 timer 的時間閥值為 1,若在前文 uv__run_timer(loop) 中,系統(tǒng)時間調(diào)用和時間比較的過程總耗時沒有超過 1ms 的話,在 timer 階段會發(fā)現(xiàn)沒有過期的計時器,那么當前 timer 就不會執(zhí)行,接下來到 check 階段,就會執(zhí)行 setImmediate 回調(diào),此時的執(zhí)行順序是:setImmediate -> setTimeout。

但是如果總耗時超過一毫秒的話,執(zhí)行順序就會發(fā)生變化,在 timer 階段,取出過期的 setTimeout 任務執(zhí)行,然后到 check 階段,再執(zhí)行 setImmediate ,此時 setTimeout -> setImmediate。

造成這種情況發(fā)生的原因是:timer 的時間檢查距當前事件循環(huán) tick 的間隔可能小于 1ms 也可能大于 1ms 的閾值,所以決定了 setTimeout 在第一次事件循環(huán)執(zhí)行與否。

接下來我用代碼阻塞的情況,會大概率造成 setTimeout 一直優(yōu)先于 setImmediate 執(zhí)行。

  1. /* TODO:  setTimeout & setImmediate */ 
  2. setImmediate(()=>{ 
  3.     console.log( 'setImmediate' ) 
  4. }) 
  5.  
  6. setTimeout(()=>{ 
  7.     console.log('setTimeout'
  8. },0) 
  9. /* 用 100000 循環(huán)阻塞代碼,促使 setTimeout 過期 */ 
  10. for(let i=0;i<100000;i++){ 

效果:

100000 循環(huán)阻塞代碼,這樣會讓 setTimeout 超過時間閥值執(zhí)行,這樣就保證了每次先執(zhí)行 setTimeout -> setImmediate 。

特殊情況:確定順序一致性。我們看一下特殊的情況。

  1. const fs = require('fs'
  2. fs.readFile('./file.js',()=>{ 
  3.     setImmediate(()=>{ 
  4.         console.log( 'setImmediate' ) 
  5.     }) 
  6.     setTimeout(()=>{ 
  7.         console.log('setTimeout'
  8.     },0) 
  9. }) 

如上情況就會造成,setImmediate 一直優(yōu)先于 setTimeout 執(zhí)行,至于為什么,來一起分析一下原因。

  • 首先分析一下異步任務——主進程中有一個異步 I/O 任務,I/O 回調(diào)中有一個 setImmediate 和 一個 setTimeout 。
  • 在 poll 階段會執(zhí)行 I/O 回調(diào)。然后處理一個 setImmediate

萬變不離其宗,只要掌握了如上各個階段的特性,那么對于不同情況的執(zhí)行情況,就可以清晰的分辨出來。

9 close 階段

close 階段用于執(zhí)行一些關閉的回調(diào)函數(shù)。執(zhí)行所有的 close 事件。接下來看一下 close 事件 libuv 的實現(xiàn)。

libuv/src/unix/core.c

  1. static void uv__run_closing_handles(uv_loop_t* loop) { 
  2.   uv_handle_t* p; 
  3.   uv_handle_t* q; 
  4.  
  5.   p = loop->closing_handles; 
  6.   loop->closing_handles = NULL
  7.  
  8.   while (p) { 
  9.     q = p->next_closing; 
  10.     uv__finish_close(p); 
  11.     p = q; 
  12.   } 
  • uv__run_closing_handles 這個方法循環(huán)執(zhí)行 close 隊列里面的回調(diào)函數(shù)。

10 Nodejs 事件循環(huán)總結

接下來總結一下 Nodejs 事件循環(huán)。

  • Nodejs 的事件循環(huán)分為 6 大階段。分別為 timer 階段,pending 階段,prepare 階段,poll 階段, check 階段,close 階段。
  • nextTick 隊列和 Microtasks 隊列執(zhí)行特點,在每一階段完成后執(zhí)行, nextTick 優(yōu)先級大于 Microtasks ( Promise )。
  • poll 階段主要處理 I/O,如果沒有其他任務,會處于輪詢阻塞階段。
  • timer 階段主要處理定時器/延時器,它們并非準確的,而且創(chuàng)建需要額外的性能浪費,它們的執(zhí)行還收到 poll 階段的影響。
  • pending 階段處理 I/O 過期的回調(diào)任務。
  • check 階段處理 setImmediate。setImmediate 和 setTimeout 執(zhí)行時機和區(qū)別。

四 Nodejs事件循環(huán)習題演練

接下來為了更清楚事件循環(huán)流程,這里出兩道事件循環(huán)的問題。作為實踐:

習題一

  1. process.nextTick(function(){ 
  2.     console.log('1'); 
  3. }); 
  4. process.nextTick(function(){ 
  5.     console.log('2'); 
  6.      setImmediate(function(){ 
  7.         console.log('3'); 
  8.     }); 
  9.     process.nextTick(function(){ 
  10.         console.log('4'); 
  11.     }); 
  12. }); 
  13.  
  14. setImmediate(function(){ 
  15.     console.log('5'); 
  16.      process.nextTick(function(){ 
  17.         console.log('6'); 
  18.     }); 
  19.     setImmediate(function(){ 
  20.         console.log('7'); 
  21.     }); 
  22. }); 
  23.  
  24. setTimeout(e=>{ 
  25.     console.log(8); 
  26.     new Promise((resolve,reject)=>{ 
  27.         console.log(8+'promise'); 
  28.         resolve(); 
  29.     }).then(e=>{ 
  30.         console.log(8+'promise+then'); 
  31.     }) 
  32. },0) 
  33.  
  34. setTimeout(e=>{ console.log(9); },0) 
  35.  
  36. setImmediate(function(){ 
  37.     console.log('10'); 
  38.     process.nextTick(function(){ 
  39.         console.log('11'); 
  40.     }); 
  41.     process.nextTick(function(){ 
  42.         console.log('12'); 
  43.     }); 
  44.     setImmediate(function(){ 
  45.         console.log('13'); 
  46.     }); 
  47. }); 
  48.  
  49. console.log('14'); 
  50.  new Promise((resolve,reject)=>{ 
  51.     console.log(15); 
  52.     resolve(); 
  53. }).then(e=>{ 
  54.     console.log(16); 
  55. }) 

如果剛看這個 demo 可以會發(fā)蒙,不過上述講到了整個事件循環(huán),再來看這個問題就很輕松了,下面來分析一下整體流程:

  • 第一階段:首先開始啟動 js 文件,那么進入第一次事件循環(huán),那么先會執(zhí)行同步任務:

最先打印:

打印console.log('14');

打印console.log(15);

nextTick 隊列:

nextTick -> console.log(1) nextTick -> console.log(2) -> setImmediate(3) -> nextTick(4)

Promise隊列

Promise.then(16)

check隊列

setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13)

timer隊列

setTimeout(8) -> promise(8+'promise') -> promise.then(8+'promise+then') setTimeout(9)

  • 第二階段:在進入新的事件循環(huán)之前,清空 nextTick 隊列,和 promise 隊列,順序是 nextTick 隊列大于 Promise 隊列。

清空 nextTick ,打印:

console.log('1');

console.log('2'); 執(zhí)行第二個 nextTick 的時候,又有一個 nextTick ,所以會把這個 nextTick 也加入到隊列中。接下來馬上執(zhí)行。console.log('4')

接下來清空Microtasks

console.log(16);

此時的 check 隊列加入了新的 setImmediate。

check隊列setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13) setImmediate(3)

  • 然后進入新的事件循環(huán),首先執(zhí)行 timer 里面的任務。執(zhí)行第一個 setTimeout。

執(zhí)行第一個 timer:

console.log(8); 此時發(fā)現(xiàn)一個 Promise 。在正常的執(zhí)行上下文中:console.log(8+'promise'); 然后將 Promise.then 加入到 nextTick 隊列中。接下里會馬上清空 nextTick 隊列。console.log(8+'promise+then');

執(zhí)行第二個 timer:

console.log(9)

接下來到了 check 階段,執(zhí)行 check 隊列里面的內(nèi)容:

執(zhí)行第一個 check:

console.log(5); 此時發(fā)現(xiàn)一個 nextTick ,然后還有一個 setImmediate 將 setImmediate 加入到 check 隊列中。然后執(zhí)行 nextTick 。console.log(6)

執(zhí)行第二個 check

console.log(10)

此時發(fā)現(xiàn)兩個 nextTick 和一個 setImmediate 。接下來清空 nextTick 隊列。將 setImmediate 添加到隊列中。

console.log(11)

console.log(12)

此時的 check 隊列是這樣的:

setImmediate(3) setImmediate(7) setImmediate(13)

接下來按順序清空 check 隊列。打印

console.log(3)

console.log(7)

console.log(13)

到此為止,執(zhí)行整個事件循環(huán)。那么整體打印內(nèi)容如下:

五 總結

本文主要講的內(nèi)容如下:

  • 異步 I/O 介紹及其內(nèi)部原理。
  • Nodejs 的事件循環(huán),六大階段。
  • Nodejs 中 setTimeout ,setImmediate , 異步 i/o ,nextTick ,Promise 的原理及其區(qū)別。
  • Nodejs 事件循環(huán)實踐。

參考資料

 

  • 從 libuv 看 nodejs 事件循環(huán)
  • 深入淺出Nodejs
  • Node.js 事件循環(huán)的工作流程 & 生命周期

 

責任編輯:武曉燕 來源: 前端Sharing
相關推薦

2021-04-27 11:28:21

React.t事件元素

2025-11-03 01:00:00

2023-12-26 12:18:02

Java設計開發(fā)

2025-03-07 10:14:03

2019-12-17 14:45:17

瀏覽器事件循環(huán)前端

2024-09-18 13:57:15

2024-10-11 09:27:52

2018-10-08 15:22:36

IO模型

2024-08-09 08:41:14

2023-08-27 21:29:43

JVMFullGC調(diào)優(yōu)

2025-02-03 07:00:00

Java接口工具

2024-06-05 11:43:10

2021-09-06 10:21:27

JavaScript表單對象 前端

2020-02-21 14:35:57

JavaScript繼承前端

2024-12-30 00:00:05

2024-08-26 08:58:50

2025-06-05 03:11:00

2021-09-15 06:55:34

異步LinqC#

2023-07-26 08:22:17

JavaIO流

2025-04-28 01:22:45

點贊
收藏

51CTO技術棧公眾號

亚洲人成777| 亚洲三区在线播放| 97精品国产一区二区三区| 欧美日本一区二区三区四区 | 凹凸日日摸日日碰夜夜爽1| jizz在线观看中文| 国产v综合v亚洲欧| 国产精品久久久久av免费| 免费一级a毛片夜夜看| 亚洲三级网址| 欧美一区二区三区四区在线观看| 国产h视频在线播放| 欧美69xxx| 26uuu精品一区二区在线观看| 国产一区二区色| 日韩女同强女同hd| 五月天久久久| 亚洲人成电影网| 99riav国产精品视频| 九九九精品视频| 天天av天天翘天天综合网色鬼国产| 亚洲欧洲中文| 91九色国产社区在线观看| 三级av免费看| 精品日本视频| 欧美色另类天堂2015| 日韩不卡一二区| av色图一区| 久久婷婷国产综合国色天香| 春色成人在线视频| 97国产精品久久久| 日本91福利区| 日产精品久久久一区二区福利| 国产第100页| 亚洲激情五月| 色悠悠久久久久| 蜜乳av中文字幕| 久久不见久久见国语| 亚洲国产日韩欧美在线图片| 亚洲丝袜在线观看| 亚洲欧美久久精品| 欧美日韩精品一区二区天天拍小说 | 久久免费在线观看| 国产又黄又爽又无遮挡| 婷婷激情图片久久| 日韩在线中文字幕| 国产黄a三级三级| 国产日产精品_国产精品毛片| 亚洲精品国精品久久99热| 中文字幕制服丝袜| 91精品啪在线观看国产爱臀| 欧美一级在线视频| 蜜桃视频无码区在线观看| 精品一级视频| 日韩一区二区不卡| 免费看91视频| 国产精品调教| jlzzjlzz国产精品久久| 欧美日韩视频专区在线播放| 99精品视频播放| 成人美女黄网站| 欧美性xxxx| 欧美激情国产精品日韩| 三上悠亚国产精品一区二区三区| 欧美性xxxxxxx| 久久综合久久色| 国产成人精品亚洲日本在线观看| 在线观看视频一区二区| 亚洲欧美激情网| 欧美黑粗硬大| 日韩三级免费观看| 亚洲第一黄色网址| 免费av一区二区三区四区| 国产一区二区久久精品| 91大神福利视频| 我不卡影院28| 久久久久久久久久久免费| 久久婷婷一区二区| 亚洲永久视频| 国产精品久久久久久五月尺| 91影院在线播放| 国产传媒一区在线| 黄色小网站91| 国产人成在线观看| 亚洲人吸女人奶水| 欧美日本视频在线观看| 69堂免费精品视频在线播放| 欧美日韩久久久久久| 超碰人人cao| 小说区图片区色综合区| 色爱av美腿丝袜综合粉嫩av | 亚洲人成人一区二区在线观看| 今天免费高清在线观看国语| 国产伦理精品| 精品视频999| 亚洲911精品成人18网站| 伦理一区二区三区| 中文日韩在线视频| 欧美综合第一页| 我不卡一区二区| 久久久久久久久国产一区| 午夜精品久久久久久99热软件| 成人一级免费视频| 国产成人在线色| 日本电影一区二区三区| 污网站在线免费看| 色婷婷久久久久swag精品| 黄色三级视频在线播放| 一区二区三区视频免费观看| 欧美成人激情图片网| 国内精品福利视频| 国产精品一区二区你懂的| 久久亚洲高清| 影音先锋男人在线资源| 欧洲激情一区二区| 中文字幕在线视频播放| 久久精品一区二区不卡| 欧美一级视频在线观看| 国产xxxx在线观看| 国产精品久久久久一区| 久久久久久久久久久福利| 99视频这里有精品| 伊人伊成久久人综合网站| 日本少妇毛茸茸高潮| 国产在线视视频有精品| 日韩在线观看电影完整版高清免费| 丰满诱人av在线播放| 3d成人h动漫网站入口| 9.1成人看片免费版| 激情综合自拍| 91久久极品少妇xxxxⅹ软件| 人人干在线视频| 欧美性猛交99久久久久99按摩| 激情小说欧美色图| 2023国产精品久久久精品双| 国产精品www网站| 神马久久高清| 五月婷婷激情综合网| 韩国三级丰满少妇高潮| 国产精品99一区二区三| 91国偷自产一区二区开放时间 | 91成人在线播放| 成人av无码一区二区三区| 国产精品久久久久久久第一福利| 成人小视频在线看| 亚洲国产合集| 日本久久久久久久久久久| 色视频在线观看免费| 亚洲国产精品人人做人人爽| 国内自拍偷拍视频| 国自产拍偷拍福利精品免费一| 亚洲在线免费观看| 免费网站看v片在线a| 欧美日韩免费高清一区色橹橹| 国产三级在线观看完整版| 日韩中文字幕亚洲一区二区va在线| 久久综合一区二区三区| 亚洲一区资源| 国产丝袜高跟一区| 婷婷激情五月综合| 国产农村妇女精品| 色啦啦av综合| 综合精品一区| 91在线看www| 在线观看wwwxxxx| 亚洲成人aaa| 91看片在线播放| 久久日韩粉嫩一区二区三区| 青青草av网站| 国产精品成久久久久| 亚洲自拍偷拍在线| 成人影音在线| 亚洲精品永久免费精品| 中文字幕一区二区三区人妻四季| 国产精品久久夜| 中文av字幕在线观看| 综合视频在线| 老司机精品福利在线观看| 久久夜夜操妹子| 日日噜噜噜夜夜爽亚洲精品| 国产日韩一级片| 图片区日韩欧美亚洲| 亚洲国产天堂久久综合网| www欧美com| 高潮精品一区videoshd| 男女av免费观看| 忘忧草精品久久久久久久高清| 97夜夜澡人人双人人人喊| 男人天堂视频在线观看| 中文字幕在线亚洲| 韩国中文字幕hd久久精品| 色综合天天性综合| 三级av在线免费观看| av在线播放成人| 高清一区在线观看| 国内精品久久久久久久97牛牛 | 亚洲欧美偷拍自拍| 国产欧美日韩伦理| 黄页免费欧美| 91国内免费在线视频| 欧洲日本在线| 亚洲国产婷婷香蕉久久久久久| 欧美另类高清videos的特点| 亚洲成人一区二区在线观看| xxxx日本黄色| 本田岬高潮一区二区三区| 三级av免费观看| 一本久道综合久久精品| 中文字幕日韩精品久久| 综合综合综合综合综合网| 亚洲free性xxxx护士hd| 欧美1级2级| 午夜精品久久久久久久久久久久久 | 国产91在线精品| 91精品国产91久久久| 快射视频在线观看| 亚洲视频视频在线| 日韩中文字幕免费在线观看| 欧美男女性生活在线直播观看| 欧美a∨亚洲欧美亚洲| 一区二区三区四区视频精品免费| 黄色片在线观看免费| 99视频精品全部免费在线| 日韩欧美中文视频| 奇米一区二区三区| 亚洲欧美日韩国产成人精品影院 | www.-级毛片线天内射视视| 伊人春色精品| 久久精品日产第一区二区三区精品版 | 尤物视频在线看| 色伦专区97中文字幕| 欧美女优在线| 亚洲精品国产拍免费91在线| 性中国xxx极品hd| 在线成人av网站| 亚洲系列在线观看| 欧美三级中文字幕| 午夜视频网站在线观看| 色综合激情久久| 东京热一区二区三区四区| 精品久久久免费| 日韩黄色在线视频| 午夜精品久久久久久久久久| 久久久久久久久久一区二区三区| 亚洲免费资源在线播放| 国产精品久久久久久久精| 最新国产成人在线观看| 人与动物性xxxx| 国产精品看片你懂得| 蜜桃av免费在线观看| 国产精品私房写真福利视频| 日韩av片在线| 国产精品对白交换视频 | 天堂а在线中文在线无限看推荐| 亚洲成人网久久久| 日本免费网站在线观看| 日韩成人免费视频| 日本一级在线观看| 亚洲美女性视频| 久蕉依人在线视频| 伊人久久男人天堂| 黄色国产网站在线播放| 欧美日韩aaaa| 92久久精品| 热99在线视频| 午夜精品成人av| 国产九九精品视频| 日韩在线精品强乱中文字幕| 国产精品久久一区二区三区| 日韩成人一级| 日韩欧美99| 亚洲成人最新网站| 免费人成在线观看视频播放| 亚洲欧洲一区| 国产成人无码av在线播放dvd| 美女网站视频久久| www日本在线观看| 久久综合色一综合色88| 毛片久久久久久| 玉米视频成人免费看| 在线观看日韩中文字幕| 欧美日韩aaa| 国产 日韩 欧美 精品| 精品夜色国产国偷在线| 99re在线视频| 欧美激情在线一区| 亚洲精品中文字幕| 成人欧美一区二区三区在线湿哒哒 | 手机毛片在线观看| 亚洲免费观看视频| 国产高潮久久久| 51久久夜色精品国产麻豆| 少妇人妻一区二区| 神马国产精品影院av| sm捆绑调教国产免费网站在线观看 | 国偷自产一区二区免费视频| 成人性生交xxxxx网站| 老司机在线精品视频| 一区二区三区视频| 亚洲韩日在线| 黄大色黄女片18第一次| 波多野结衣中文字幕一区二区三区| av电影网站在线观看| 夜夜操天天操亚洲| 一区二区小视频| 精品亚洲男同gayvideo网站| 在线不卡日本v二区707| 国产精品久久久久久久9999 | 日韩女优毛片在线| yourporn在线观看视频| 久久久爽爽爽美女图片| 欧美视频免费看| 欧美日本韩国一区二区三区| 国产一区二区三区四区三区四| 亚洲黄色a v| 99国产精品久久久久| 欧美爱爱小视频| 欧美日韩国产综合草草| 色视频免费在线观看| 国内精品一区二区三区| 高清精品久久| 一区二区免费在线视频| 嫩草成人www欧美| 亚洲色偷偷色噜噜狠狠99网| 亚洲欧美日韩久久| 一区二区美女视频| 国产亚洲xxx| 中文字幕一区久| 久久久久欧美| 精品成人在线| 伊人影院在线观看视频| 中文字幕中文字幕在线一区| 一二三区免费视频| 日韩激情在线视频| av资源在线| 国产精品一区二区三区免费观看| 中文字幕免费一区二区| 日日夜夜精品视频免费观看| 国产精品国产自产拍高清av王其| 国产免费一区二区三区四区五区| 日韩精品福利网站| 天堂а√在线最新版中文在线| 好看的日韩精品| 亚洲日本黄色| 久久久午夜精品福利内容| 亚洲影院在线观看| 亚洲精品久久久久久动漫器材一区 | 国产精品9999久久久久仙踪林| 一区二区不卡| 成人在线短视频| 亚洲一区二区三区四区在线观看| 精品国产亚洲av麻豆| 欧美刺激性大交免费视频| 免费精品一区| 日韩视频 中文字幕| 成人免费视频免费观看| 日本五十熟hd丰满| 亚洲精品720p| 欧美黄色网页| 亚洲人一区二区| 国产精品91xxx| 国产真实的和子乱拍在线观看| 精品久久人人做人人爽| av今日在线| 欧美亚洲国产免费| 另类的小说在线视频另类成人小视频在线 | 九九九热精品免费视频观看网站| 一区二区精彩视频| 99视频在线免费播放| 国产婷婷一区二区| 亚洲午夜激情视频| 欧美激情一区二区三区久久久 | 国产午夜精品无码一区二区| 欧美精品一区二区三| 美女福利一区二区三区| 亚洲图片小说在线| 国产盗摄精品一区二区三区在线| 日韩精品一区二区三| 亚洲色图15p| 日韩08精品| 久久精品.com| 国产精品国产精品国产专区不蜜 | 成人激情综合网站| 天天综合久久综合| 久久这里有精品| 欧美成人基地| 色一情一区二区三区| 亚洲mv大片欧洲mv大片精品| 成人影视在线播放| 97伦理在线四区| 三级在线观看一区二区| 国产老头老太做爰视频| 日韩精品免费在线视频| 日韩免费在线电影| 国产亚洲综合视频| 成人免费一区二区三区在线观看| 亚洲欧美日韩动漫| 91美女片黄在线观| 噜噜噜91成人网| 久久久无码精品亚洲国产|