久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

    1. 站長(zhǎng)資訊網(wǎng)
      最全最豐富的資訊網(wǎng)站

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      本篇文章是Nodejs的進(jìn)階學(xué)習(xí),帶大家詳細(xì)了解一下Nodejs中的異步I/O和事件循環(huán),希望對(duì)大家有所幫助!

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      本文講詳細(xì)講解 nodejs 中兩個(gè)比較難以理解的部分異步I/O事件循環(huán),對(duì) nodejs 核心知識(shí)點(diǎn),做梳理和補(bǔ)充?!就扑]學(xué)習(xí):《nodejs 教程》】

      送人玫瑰,手有余香,希望閱讀后感覺(jué)不錯(cuò)的同學(xué),可以給點(diǎn)個(gè)贊,鼓勵(lì)我繼續(xù)創(chuàng)作前端硬文。

      老規(guī)矩我們帶上疑問(wèn)開(kāi)始今天的分析:

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

      異步I/O

      概念

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

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

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

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

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

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

      nodejs中同步和異步IO模式

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

      同步I/O模式

      /* TODO:  阻塞 */ const fs = require('fs'); const data = fs.readFileSync('./file.js'); console.log(data)
      • 代碼阻塞 :讀取同級(jí)目錄下的 file.js 文件,結(jié)果 databuffer 結(jié)構(gòu),這樣當(dāng)讀取過(guò)程中,會(huì)阻塞代碼的執(zhí)行,所以 console.log(data) 將被阻塞,只有當(dāng)結(jié)果返回的時(shí)候,才能正常打印 data 。
      • 異常處理 :如上操作有一個(gè)致命點(diǎn)就是,如果出現(xiàn)了異常,(比如在同級(jí)目錄下沒(méi)有 file.js 文件),就會(huì)讓整個(gè)程序報(bào)錯(cuò),接下來(lái)的代碼講不會(huì)執(zhí)行。通常需要 try catch來(lái)捕獲錯(cuò)誤邊界。代碼如下:
      /* TODO: 阻塞 - 捕獲異常  */ try{     const fs = require('fs');     const data = fs.readFileSync('./file1.js');     console.log(data) }catch(e){     console.log('發(fā)生錯(cuò)誤:',e) } console.log('正常執(zhí)行')
      • 如上即便發(fā)生了錯(cuò)誤,也不會(huì)影響到后續(xù)代碼的執(zhí)行以及應(yīng)用程序發(fā)生錯(cuò)誤導(dǎo)致的退出。

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      異步I/O模式

      這就是剛剛介紹的異步I/O。首先看一下異步模式下的 I/O 操作:

      /* TODO: 非阻塞 - 異步 I/O */ const fs = require('fs') fs.readFile('./file.js',(err,data)=>{     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> }) console.log(111) // 111 先被打印~  fs.readFile('./file1.js',(err,data)=>{     console.log(err,data) // 保存  [ no such file or directory, open './file1.js'] ,找不到文件。 })
      • 回調(diào) callback 被異步執(zhí)行,返回的第一個(gè)參數(shù)是錯(cuò)誤信息,如果沒(méi)有錯(cuò)誤,那么返回 null ,第二個(gè)參數(shù)為 fs.readFile 執(zhí)行得到的真正內(nèi)容。
      • 這種異步的形式可以會(huì)優(yōu)雅的捕獲到執(zhí)行 I/O 中出現(xiàn)的錯(cuò)誤,比如說(shuō)如上當(dāng)讀取 file1.js 文件時(shí)候,出現(xiàn)了找不到對(duì)應(yīng)文件的異常行為,會(huì)直接通過(guò)第一個(gè)參數(shù)形式傳遞到 callback 中。

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

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

      事件循環(huán)

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

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

      觀察者

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

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

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

      在 Nodejs 中,對(duì)應(yīng)觀察者接收對(duì)應(yīng)類(lèi)型的事件,事件循環(huán)過(guò)程中,會(huì)向這些觀察者詢問(wèn)有沒(méi)有該執(zhí)行的任務(wù),如果有,那么觀察者會(huì)取出任務(wù),交給事件循環(huán)去執(zhí)行。

      請(qǐng)求對(duì)象與線程池

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

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

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

      異步 I/O 操作機(jī)制

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

      用一幅圖表示四者的關(guān)系:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      總結(jié)上述過(guò)程:

      • 第一階段:每一次異步 I/O 的調(diào)用,首先在 nodejs 底層設(shè)置請(qǐng)求參數(shù)和回調(diào)函 callback,形成請(qǐng)求對(duì)象

      • 第二階段:形成的請(qǐng)求對(duì)象,會(huì)被放入線程池,如果線程池有空閑的 I/O 線程,會(huì)執(zhí)行此次 I/O 任務(wù),得到結(jié)果。

      • 第三階段:事件循環(huán)I/O 觀察者,會(huì)從請(qǐng)求對(duì)象中找到已經(jīng)得到結(jié)果的 I/O 請(qǐng)求對(duì)象,取出結(jié)果和回調(diào)函數(shù),將回調(diào)函數(shù)放入事件循環(huán)中,執(zhí)行回調(diào),完成整個(gè)異步 I/O 任務(wù)。

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

      事件循環(huán)

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

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

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

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

      我總結(jié)了流程圖如下所示:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

      • setTimeoutsetInterval 延時(shí)器計(jì)時(shí)器。
      • 異步 I/O 任務(wù):文件任務(wù) ,網(wǎng)絡(luò)請(qǐng)求等。
      • setImmediate 任務(wù)。
      • process.nextTick 任務(wù)。
      • Promise 微任務(wù)。

      接下來(lái)會(huì)一一講到 ,這些任務(wù)的原理以及 nodejs 是如何處理這些任務(wù)的。

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

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

      /*    ┌───────────────────────────┐ ┌─>│           timers          │     -> 定時(shí)器,延時(shí)器的執(zhí)行     │  └─────────────┬─────────────┘ │  ┌─────────────┴─────────────┐ │  │     pending callbacks     │     -> i/o │  └─────────────┬─────────────┘ │  ┌─────────────┴─────────────┐ │  │       idle, prepare       │ │  └─────────────┬─────────────┘      ┌───────────────┐ │  ┌─────────────┴─────────────┐      │   incoming:   │ │  │           poll            │<─────┤  connections, │ │  └─────────────┬─────────────┘      │   data, etc.  │ │  ┌─────────────┴─────────────┐      └───────────────┘ │  │           check           │ │  └─────────────┬─────────────┘ │  ┌─────────────┴─────────────┐ └──┤      close callbacks      │    └───────────────────────────┘ */
      • 第一階段: timer ,timer 階段主要做的事是,執(zhí)行 setTimeoutsetInterval 注冊(cè)的回調(diào)函數(shù)。

      • 第二階段:pending callback ,大部分 I/O 回調(diào)任務(wù)都是在 poll 階段執(zhí)行的,但是也會(huì)存在一些上一次事件循環(huán)遺留的被延時(shí)的 I/O 回調(diào)函數(shù),那么此階段就是為了調(diào)用之前事件循環(huán)延遲執(zhí)行的 I/O 回調(diào)函數(shù)。

      • 第三階段:idle prepare 階段,僅用于 nodejs 內(nèi)部模塊的使用。

      • 第四階段:poll 輪詢階段,這個(gè)階段主要做兩件事,一這個(gè)階段會(huì)執(zhí)行異步 I/O 的回調(diào)函數(shù); 二 計(jì)算當(dāng)前輪詢階段阻塞后續(xù)階段的時(shí)間。

      • 第五階段:check階段,當(dāng) poll 階段回調(diào)函數(shù)隊(duì)列為空的時(shí)候,開(kāi)始進(jìn)入 check 階段,主要執(zhí)行 setImmediate 回調(diào)函數(shù)。

      • 第六階段:close階段,執(zhí)行注冊(cè) close 事件的回調(diào)函數(shù)。

      對(duì)于每一個(gè)階段的執(zhí)行特點(diǎn)和對(duì)應(yīng)的事件任務(wù),我接下來(lái)會(huì)詳細(xì)剖析。我們看一下六個(gè)階段在底層源碼中是怎么樣體現(xiàn)的。

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

      libuv/src/unix/core.c

      int uv_run(uv_loop_t* loop, uv_run_mode mode) {   // 省去之前的流程。   while (r != 0 && loop->stop_flag == 0) {      /* 更新事件循環(huán)的時(shí)間 */      uv__update_time(loop);      /*第一階段: timer 階段執(zhí)行  */     uv__run_timers(loop);      /*第二階段: pending 階段 */     ran_pending = uv__run_pending(loop);      /*第三階段: idle prepare 階段 */     uv__run_idle(loop);     uv__run_prepare(loop);      timeout = 0;     if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)      /* 計(jì)算 timeout 時(shí)間  */       timeout = uv_backend_timeout(loop);          /* 第四階段:poll 階段 */     uv__io_poll(loop, timeout);      /* 第五階段:check 階段 */     uv__run_check(loop);     /* 第六階段: close 階段  */     uv__run_closing_handles(loop);     /* 判斷當(dāng)前線程還有任務(wù) */       r = uv__loop_alive(loop);      /* 省去之后的流程 */   }   return r; }
      • 我們看到六個(gè)階段是按序執(zhí)行的,只有完成上一階段的任務(wù),才能進(jìn)行下一階段
      • 當(dāng) uv__loop_alive 判斷當(dāng)前事件循環(huán)沒(méi)有任務(wù),那么退出線程。

      2 任務(wù)隊(duì)列

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

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

      libuv 處理任務(wù)隊(duì)列

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

      • timer 隊(duì)列( PriorityQueue ):本質(zhì)上的數(shù)據(jù)結(jié)構(gòu)是二叉最小堆,二叉最小堆的根節(jié)點(diǎn)獲取最近的時(shí)間線上的 timer 對(duì)應(yīng)的回調(diào)函數(shù)。

      • I/O 事件隊(duì)列:存放 I/O 任務(wù)。

      • Immediate 隊(duì)列( ImmediateList ):多個(gè) Immediate ,node 層用鏈表數(shù)據(jù)結(jié)構(gòu)儲(chǔ)存。

      • 關(guān)閉回調(diào)事件隊(duì)列:放置待 close 的回調(diào)函數(shù)。

      非 libuv 中間隊(duì)列

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

      中間隊(duì)列的執(zhí)行特點(diǎn):

      • 首先要明白兩個(gè)中間隊(duì)列并非在 libuv 中被執(zhí)行,它們都是在 nodejs 層執(zhí)行的,在 libuv 層處理每一個(gè)階段的任務(wù)之后,會(huì)和 node 層進(jìn)行通訊,那么會(huì)優(yōu)先處理兩個(gè)隊(duì)列中的任務(wù)。

      • nextTick 任務(wù)的優(yōu)先級(jí)要大于 Microtasks 任務(wù)中的 Promise 回調(diào)。也就是說(shuō) node 會(huì)首先清空 nextTick 中的任務(wù),然后才是 Promise 中的任務(wù)。為了驗(yàn)證這個(gè)結(jié)論,例舉一個(gè)打印結(jié)果的題目如下:

      /* TODO: 打印順序  */ setTimeout(()=>{     console.log('setTimeout 執(zhí)行') },0)  const p = new Promise((resolve)=>{      console.log('Promise執(zhí)行')      resolve() }) p.then(()=>{     console.log('Promise 回調(diào)執(zhí)行') })  process.nextTick(()=>{     console.log('nextTick 執(zhí)行') }) console.log('代碼執(zhí)行完畢')

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

      效果:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

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

      • 無(wú)論是 nextTick 的任務(wù),還是 promise 中的任務(wù), 兩個(gè)任務(wù)中的代碼會(huì)阻塞事件循環(huán)的有序進(jìn)行,導(dǎo)致 I/O 餓死的情況發(fā)生,所以需要謹(jǐn)慎處理兩個(gè)任務(wù)中的邏輯。比如如下:
      /* TODO: 阻塞 I/O 情況 */ process.nextTick(()=>{     const now = +new Date()     /* 阻塞代碼三秒鐘 */     while( +new Date() < now + 3000 ){} })  fs.readFile('./file.js',()=>{     console.log('I/O: file ') })  setTimeout(() => {     console.log('setTimeout: ') }, 0);

      效果:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

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

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      4 timer 階段 -> 計(jì)時(shí)器 timer / 延時(shí)器 interval

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

      libuv 層如何處理 timer

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

      libuv/src/timer.c

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

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

      node 層如何處理 timer

      在 Nodejs 中 setTimeoutsetInterval 是 nodejs 自己實(shí)現(xiàn)的,來(lái)一起看一下實(shí)現(xiàn)細(xì)節(jié):

      node/lib/timers.js

      function setTimeout(callback,after){     //...     /* 判斷參數(shù)邏輯 */     //..     /* 創(chuàng)建一個(gè) timer 觀察者 */     const timeout = new Timeout(callback, after, args, false, true);     /* 將 timer 觀察者插入到 timer 堆中  */     insert(timeout, timeout._idleTimeout);      return timeout; }
      • setTimeout: 邏輯很簡(jiǎn)單,就是創(chuàng)建一個(gè) timer 時(shí)間觀察者,然后放入計(jì)時(shí)器堆中。

      那么 Timeout 做了些什么呢?

      node/lib/internal/timers.js

      function Timeout(callback, after, args, isRepeat, isRefed) {   after *= 1    if (!(after >= 1 && after <= 2 ** 31 - 1)) {     after = 1 // 如果延時(shí)器 timeout 為 0 ,或者是大于 2 ** 31 - 1 ,那么設(shè)置成 1    }   this._idleTimeout = after; // 延時(shí)時(shí)間    this._idlePrev = this;   this._idleNext = this;   this._idleStart = null;   this._onTimeout = null;   this._onTimeout = callback; // 回調(diào)函數(shù)   this._timerArgs = args;   this._repeat = isRepeat ? after : null;   this._destroyed = false;      initAsyncResource(this, 'Timeout'); }
      • 在 nodejs 中無(wú)論 setTimeout 還是 setInterval 本質(zhì)上都是 Timeout 類(lèi)。超出最大時(shí)間閥 2 ** 31 - 1 或者 setTimeout(callback, 0) ,_idleTimeout 會(huì)被設(shè)置成 1 ,轉(zhuǎn)換為 setTimeout(callback, 1) 來(lái)執(zhí)行。

      timer 處理流程圖

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      timer 特性

      這里有兩點(diǎn)需要注意:

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

      驗(yàn)證結(jié)論一次執(zhí)行一個(gè) timer 任務(wù) ,先來(lái)看一段代碼片段:

      setTimeout(()=>{     console.log('setTimeout1:')     process.nextTick(()=>{         console.log('nextTick')     }) },0) setTimeout(()=>{     console.log('setTimeout2:') },0)

      打印結(jié)果:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

      • 精度問(wèn)題 :關(guān)于 setTimeout 的計(jì)數(shù)器問(wèn)題,計(jì)時(shí)器并非精確的,盡管在 nodejs 的事件循環(huán)非常的快,但是從延時(shí)器 timeout 類(lèi)的創(chuàng)建,會(huì)占用一些事件,再到上下文執(zhí)行, I/O 的執(zhí)行,nextTick 隊(duì)列執(zhí)行,Microtasks 執(zhí)行,都會(huì)阻塞延時(shí)器的執(zhí)行。甚至在檢查 timer 過(guò)期的時(shí)候,也會(huì)消耗一些 cpu 時(shí)間。

      • 性能問(wèn)題 :如果想用 setTimeout(fn,0) 來(lái)執(zhí)行一些非立即調(diào)用的任務(wù),那么性能上不如 process.nextTick 實(shí)在,首先 setTimeout 精度不夠,還有一點(diǎn)就是里面有定時(shí)器對(duì)象,并需要在 libuv 底層執(zhí)行,占用一定性能,所以可以用 process.nextTick 解決這種場(chǎng)景。

      5 pending 階段

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

      libuv/src/unix/core.c

      static int uv__run_pending(uv_loop_t* loop) {   QUEUE* q;   QUEUE pq;   uv__io_t* w   /* pending_queue 為空,清空隊(duì)列 ,返回 0  */   if (QUEUE_EMPTY(&loop->pending_queue))     return 0;      QUEUE_MOVE(&loop->pending_queue, &pq);   while (!QUEUE_EMPTY(&pq)) { /* pending_queue 不為空的情況,清空 I/O 回調(diào)。返回 1  */     q = QUEUE_HEAD(&pq);     QUEUE_REMOVE(q);     QUEUE_INIT(q);     w = QUEUE_DATA(q, uv__io_t, pending_queue);     w->cb(loop, w, POLLOUT);   }   return 1; }
      • 如果存放 I/O 回調(diào)的任務(wù)的 pending_queue 是空的,那么直接返回 0。
      • 如果 pending_queue 有 I/O 回調(diào)任務(wù),那么執(zhí)行回調(diào)任務(wù)。

      6 idle, prepare 階段

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

      7 poll I / O 輪詢階段

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

        timeout = 0;     if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)       /* 計(jì)算 timeout   */       timeout = uv_backend_timeout(loop);       /* 進(jìn)入 I/O 輪詢 */       uv__io_poll(loop, timeout);
      • 初始化超時(shí)時(shí)間 timeout = 0 ,通過(guò) uv_backend_timeout 計(jì)算本次 poll 階段的超時(shí)時(shí)間。超時(shí)時(shí)間會(huì)影響到異步 I/O 和后續(xù)事件循環(huán)的執(zhí)行。

      timeout代表什么

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

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

      獲取timeout

      timeout 的獲取是通過(guò) uv_backend_timeout 那么如何獲得的呢?

      int uv_backend_timeout(const uv_loop_t* loop) {     /* 當(dāng)前事件循環(huán)任務(wù)停止 ,不阻塞 */   if (loop->stop_flag != 0)     return 0;    /* 當(dāng)前事件循環(huán) loop 不活躍的時(shí)候 ,不阻塞 */   if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))     return 0;   /* 當(dāng) idle 句柄隊(duì)列不為空時(shí),返回 0,即不阻塞。 */   if (!QUEUE_EMPTY(&loop->idle_handles))     return 0;    /* i/o pending 隊(duì)列不為空的時(shí)候。 */     if (!QUEUE_EMPTY(&loop->pending_queue))     return 0;    /* 有關(guān)閉回調(diào) */   if (loop->closing_handles)     return 0;   /* 計(jì)算有沒(méi)有延時(shí)最小的延時(shí)器 | 定時(shí)器 */   return uv__next_timeout(loop); }

      uv_backend_timeout 主要做的事情是:

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

      接下來(lái)看一下 uv__next_timeout 邏輯。

      int uv__next_timeout(const uv_loop_t* loop) {   const struct heap_node* heap_node;   const uv_timer_t* handle;   uint64_t diff;   /* 找到延時(shí)時(shí)間最小的 timer  */   heap_node = heap_min((const struct heap*) &loop->timer_heap);   if (heap_node == NULL) /* 如何沒(méi)有 timer,那么返回 -1 ,一直進(jìn)入 poll 狀態(tài)  */     return -1;     handle = container_of(heap_node, uv_timer_t, heap_node);    /* 有過(guò)期的 timer 任務(wù),那么返回 0,poll 階段不阻塞 */   if (handle->timeout <= loop->time)     return 0;   /* 返回當(dāng)前最小閥值的 timer 與 當(dāng)前事件循環(huán)的事件相減,得出來(lái)的時(shí)間,可以證明 poll 可以停留多長(zhǎng)時(shí)間 */    diff = handle->timeout - loop->time;   return (int) diff; }

      uv__next_timeout 做的事情如下:

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

      執(zhí)行io_poll

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

      poll階段本質(zhì)

      接下來(lái)總結(jié)一下 poll 階段的本質(zhì):

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

      poll 階段流程圖

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      8 check 階段

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

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

      Nodejs 底層中的 setImmediate

      setImmediate定義

      node/lib/timer.js

      function setImmediate(callback, arg1, arg2, arg3) {   validateCallback(callback); /* 校驗(yàn)一下回調(diào)函數(shù) */    /* 創(chuàng)建一個(gè) Immediate 類(lèi)   */    return new Immediate(callback, args); }
      • 當(dāng)調(diào)用 setImmediate 本質(zhì)上調(diào)用 nodejs 中的 setImmediate 方法,首先校驗(yàn)回調(diào)函數(shù),然后創(chuàng)建一個(gè) Immediate 類(lèi)。接下來(lái)看一下 Immediate 類(lèi)。

      node/lib/internal/timers.js

      class Immediate{    constructor(callback, args) {     this._idleNext = null;     this._idlePrev = null; /* 初始化參數(shù) */     this._onImmediate = callback;     this._argv = args;     this._destroyed = false;     this[kRefed] = false;      initAsyncResource(this, 'Immediate');     this.ref();     immediateInfo[kCount]++;          immediateQueue.append(this); /* 添加 */   } }
      • Immediate 類(lèi)會(huì)初始化一些參數(shù),然后將當(dāng)前 Immediate 類(lèi),插入到 immediateQueue 鏈表中。
      • immediateQueue 本質(zhì)上是一個(gè)鏈表,存放每一個(gè) Immediate。

      setImmediate執(zhí)行

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

      setImmediate(()=>{     console.log('setImmediate1')     process.nextTick(()=>{         console.log('nextTick')     }) })  setImmediate(()=>{     console.log('setImmediate2') })

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

      setImmediate執(zhí)行流程圖

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      setTimeout & setImmediate

      接下來(lái)對(duì)比一下 setTimeoutsetImmediate,如果開(kāi)發(fā)者期望延時(shí)執(zhí)行的異步任務(wù),那么接下來(lái)對(duì)比一下 setTimeout(fn,0)setImmediate(fn) 區(qū)別。

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

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

      首先寫(xiě)一個(gè) demo:

      setTimeout(()=>{     console.log('setTimeout') },0)  setImmediate(()=>{     console.log( 'setImmediate' ) })

      猜測(cè)

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

      實(shí)際打印結(jié)果

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

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

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

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

      /* TODO:  setTimeout & setImmediate */ setImmediate(()=>{     console.log( 'setImmediate' ) })  setTimeout(()=>{     console.log('setTimeout') },0) /* 用 100000 循環(huán)阻塞代碼,促使 setTimeout 過(guò)期 */ for(let i=0;i<100000;i++){ }

      效果:

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

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

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

      const fs = require('fs') fs.readFile('./file.js',()=>{     setImmediate(()=>{         console.log( 'setImmediate' )     })     setTimeout(()=>{         console.log('setTimeout')     },0) })

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

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

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

      9 close 階段

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

      libuv/src/unix/core.c

      static void uv__run_closing_handles(uv_loop_t* loop) {   uv_handle_t* p;   uv_handle_t* q;    p = loop->closing_handles;   loop->closing_handles = NULL;    while (p) {     q = p->next_closing;     uv__finish_close(p);     p = q;   } }
      • uv__run_closing_handles 這個(gè)方法循環(huán)執(zhí)行 close 隊(duì)列里面的回調(diào)函數(shù)。

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

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

      • Nodejs 的事件循環(huán)分為 6 大階段。分別為 timer 階段,pending 階段,prepare 階段,poll 階段, check 階段,close 階段。

      • nextTick 隊(duì)列和 Microtasks 隊(duì)列執(zhí)行特點(diǎn),在每一階段完成后執(zhí)行, nextTick 優(yōu)先級(jí)大于 Microtasks ( Promise )。

      • poll 階段主要處理 I/O,如果沒(méi)有其他任務(wù),會(huì)處于輪詢阻塞階段。

      • timer 階段主要處理定時(shí)器/延時(shí)器,它們并非準(zhǔn)確的,而且創(chuàng)建需要額外的性能浪費(fèi),它們的執(zhí)行還收到 poll 階段的影響。

      • pending 階段處理 I/O 過(guò)期的回調(diào)任務(wù)。

      • check 階段處理 setImmediate。 setImmediate 和 setTimeout 執(zhí)行時(shí)機(jī)和區(qū)別。

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

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

      習(xí)題一

      process.nextTick(function(){     console.log('1'); }); process.nextTick(function(){     console.log('2');      setImmediate(function(){         console.log('3');     });     process.nextTick(function(){         console.log('4');     }); });  setImmediate(function(){     console.log('5');      process.nextTick(function(){         console.log('6');     });     setImmediate(function(){         console.log('7');     }); });  setTimeout(e=>{     console.log(8);     new Promise((resolve,reject)=>{         console.log(8+'promise');         resolve();     }).then(e=>{         console.log(8+'promise+then');     }) },0)  setTimeout(e=>{ console.log(9); },0)  setImmediate(function(){     console.log('10');     process.nextTick(function(){         console.log('11');     });     process.nextTick(function(){         console.log('12');     });     setImmediate(function(){         console.log('13');     }); });  console.log('14');  new Promise((resolve,reject)=>{     console.log(15);     resolve(); }).then(e=>{     console.log(16); })

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

      • 第一階段: 首先開(kāi)始啟動(dòng) js 文件,那么進(jìn)入第一次事件循環(huán),那么先會(huì)執(zhí)行同步任務(wù):

      最先打?。?/p>

      打印console.log('14');

      打印console.log(15);

      nextTick 隊(duì)列:

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

      Promise隊(duì)列

      Promise.then(16)

      check隊(duì)列

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

      timer隊(duì)列

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

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

      清空 nextTick ,打印:

      console.log('1');

      console.log('2');

      執(zhí)行第二個(gè) nextTick 的時(shí)候,又有一個(gè) nextTick ,所以會(huì)把這個(gè) nextTick 也加入到隊(duì)列中。接下來(lái)馬上執(zhí)行。

      console.log('4')

      接下來(lái)清空Microtasks

      console.log(16);

      此時(shí)的 check 隊(duì)列加入了新的 setImmediate。

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

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

      執(zhí)行第一個(gè) timer:

      console.log(8);

      此時(shí)發(fā)現(xiàn)一個(gè) Promise 。在正常的執(zhí)行上下文中:

      console.log(8+'promise');

      然后將 Promise.then 加入到 nextTick 隊(duì)列中。接下里會(huì)馬上清空 nextTick 隊(duì)列。

      console.log(8+'promise+then');

      執(zhí)行第二個(gè) timer:

      console.log(9)

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

      執(zhí)行第一個(gè) check:

      console.log(5);

      此時(shí)發(fā)現(xiàn)一個(gè) nextTick ,然后還有一個(gè) setImmediate 將 setImmediate 加入到 check 隊(duì)列中。然后執(zhí)行 nextTick 。

      console.log(6)

      執(zhí)行第二個(gè) check

      console.log(10)

      此時(shí)發(fā)現(xiàn)兩個(gè) nextTick 和一個(gè) setImmediate 。接下來(lái)清空 nextTick 隊(duì)列。將 setImmediate 添加到隊(duì)列中。

      console.log(11)

      console.log(12)

      此時(shí)的 check 隊(duì)列是這樣的:

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

      接下來(lái)按順序清空 check 隊(duì)列。打印

      console.log(3)

      console.log(7)

      console.log(13)

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

      Nodejs進(jìn)階學(xué)習(xí):深入了解異步I/O和事件循環(huán)

      總結(jié)

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

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

      原文地址:https://juejin.cn/post/7002106372200333319

      作者:我不是外星人

      贊(0)
      分享到: 更多 (0)
      網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)