中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何理解Nodejs中的事件循環

發布時間:2021-09-30 10:34:27 來源:億速云 閱讀:158 作者:柒染 欄目:web開發

這期內容當中小編將會給大家帶來有關如何理解Nodejs中的事件循環,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

Node事件循環

Node底層使用的語言libuv,是一個c++語言。他用來操作底層的操作系統,封裝了操作系統的接口。Node的事件循環也是用libuv來寫的,所以Node生命周期和瀏覽器的還是有區別的。

因為Node和操作系統打交道,所以事件循環比較復雜,也有一些自己特有的API。
事件循環在不同的操作系統里有一些細微的差異。這將涉及到操作系統的知識,暫時不表。 本次只介紹JS主線程中,Node的運作流程。Node的其他線程暫時也不擴展。

事件循環圖

說好的一張圖,也不賣關子。下邊這張圖搞清楚了,事件循環就學會了。

如何理解Nodejs中的事件循環

事件循環圖

如何理解Nodejs中的事件循環

事件循環圖-結構

為了讓大家先有個大局觀,先貼一張目錄結構圖在前邊:

如何理解Nodejs中的事件循環

目錄

接下來詳細展開說說

主線程

如何理解Nodejs中的事件循環

主線程

上圖中,幾個色塊的含義:

  • main:啟動入口文件,運行主函數

  • event loop:檢查是否要進入事件循環

    • 檢查其他線程里是否還有待處理事項

    • 檢查其他任務是否還在進行中(比如計時器、文件讀取操作等任務是否完成)

    • 有以上情況,進入事件循環,運行其他任務
      事件循環的過程:沿著從timers到close callbacks這個流程,走一圈。到event loop看是否結束,沒結束再走一圈。

  • over:所有的事情都完畢,結束

事件循環 圈

如何理解Nodejs中的事件循環

事件循環 圈

圖中灰色的圈跟操作系統有關系,不是本章解析重點。重點關注黃色、橙色的圈還有中間橘黃的方框。

我們把每一圈的事件循環叫做「一次循環」、又叫「一次輪詢」、又叫「一次Tick」。

一次循環要經過六個階段:
  • timers:計時器(setTimeout、setInterval等的回調函數存放在里邊)

  • pending callback

  • idle prepare

  • poll:輪詢隊列(除timers、check之外的回調存放在這里)

  • check:檢查階段(使用 setImmediate 的回調會直接進入這個隊列)

  • close callbacks

如何理解Nodejs中的事件循環

本次我們只關注上邊標紅的三個重點。

工作原理
  • 每一個階段都會維護一個事件隊列。可以把每一個圈想象成一個事件隊列。

  • 這就和瀏覽器不一樣了,瀏覽器最多兩個隊列(宏隊列、微隊列)。但是在node里邊有六個隊列

  • 到達一個隊列后,檢查隊列內是否有任務(也就是看下是否有回調函數)需要執行。如果有,就依次執行,直到全部執行完畢、清空隊列。

  • 如果沒有任務,進入下一個隊列去檢查。直到所有隊列檢查一遍,算一個輪詢。

  • 其中,timerspending callbackidle prepare等執行完畢后,到達poll隊列。

timers隊列的工作原理

timers并非真正意義上的隊列,他內部存放的是計時器。
每次到達這個隊列,會檢查計時器線程內的所有計時器,計時器線程內部多個計時器按照時間順序排序。

檢查過程:將每一個計時器按順序分別計算一遍,計算該計時器開始計時的時間到當前時間是否滿足計時器的間隔參數設定(比如1000ms,計算計時器開始計時到現在是否有1m)。當某個計時器檢查通過,則執行其回調函數。

poll隊列的運作方式

  • 如果poll中有回調函數需要執行,依次執行回調,直到清空隊列。

  • 如果poll中沒有回調函數需要執行,已經是空隊列了。則會在這里等待,等待其他隊列中出現回調,

    • 如果其他隊列中出現回調,則從poll向下到over,結束該階段,進入下一階段。

    • 如果其他隊列也都沒有回調,則持續在poll隊列等待,直到任何一個隊列出現回調后再進行工作。(是個小懶蟲的處事方式)

舉例梳理事件流程

setTimeout(() => {
  console.log('object');
}, 5000)
console.log('node');
以上代碼的事件流程梳理
  • 進入主線程,執行setTimeout(),回調函數作為異步任務被放入異步隊列timers隊列中,暫時不執行。

  • 繼續向下,執行定時器后邊的console,打印“node”。

  • 判斷是否有事件循環。是,走一圈輪詢:從timers - pending callback - idle prepare……

  • poll隊列停下循環并等待。

    • 由于這時候沒到5秒,timers隊列無任務,所以一直在poll隊列卡著,同時輪詢檢查其他隊列是否有任務。

  • 等5秒到達,setTimeout的回調塞到timers內,例行輪詢檢查到timers隊列有任務,則向下走,經過check、close callbacks后到達timers。將timers隊列清空。

  • 繼續輪詢到poll等待,詢問是否還需要event loop,不需要,則到達over結束。

要理解這個問題,看下邊的代碼及流程解析:
setTimeout(function t1() {
  console.log('setTimeout');
}, 5000)
console.log('node 生命周期');

const http = require('http')

const server = http.createServer(function h2() {
  console.log('請求回調');
});

server.listen(8080)

代碼分析如下:

  • 照舊,先執行主線程,打印“node 生命周期”、引入http后創建http服務。

  • 然后event loop檢查是否有異步任務,檢查發現有定時器任務和請求任務。所以進入事件循環。

  • 六個隊列都沒任務,則在poll隊列等待。如下圖:

    如何理解Nodejs中的事件循環

  • 過了五秒,timers中有了任務,則流程從poll放行向下,經過check和close callbacks隊列后,到達event loop。

  • event loop檢查是否有異步任務,檢查發現有定時器任務和請求任務。所以再次進入事件循環。

  • 到達timers隊列,發現有回調函數任務,則依次執行回調,清空timers隊列(當然這里只有一個5秒到達后的回調,所以直接執行完了即可),打印出“setTimeout”。如下圖

    如何理解Nodejs中的事件循環

  • 清空timers隊列后,輪詢繼續向下到達poll隊列,由于poll隊列現在是空隊列,所以在這里等待。

  • 后來,假設用戶請求發來了,h2回調函數被放到poll隊列。于是poll中有回調函數需要執行,依次執行回調,直到清空poll隊列。

  • poll隊列清空,此時poll隊列是空隊列,繼續等待。

    如何理解Nodejs中的事件循環

  • 由于node線程一直holding在poll隊列,等很長一段時間還是沒有任務來臨時,會自動斷開等待(不自信表現),向下執行輪詢流程,經過check、close callbacks后到達event loop

  • 到了event loop后,檢查是否有異步任務,檢查發現有請求任務。(此時定時器任務已經執行完畢,所以沒有了),則繼續再次進入事件循環。

  • 到達poll隊列,再次holding……

  • 再等很長時間沒有任務來臨,自動斷開到even loop(再補充一點無任務的循環情況)

  • 再次回到poll隊列掛起

  • 無限循環……

梳理事件循環流程圖:

注意:下圖中的“是否有任務”的說法表示“是否有本隊列的任務”。

如何理解Nodejs中的事件循環

event loop流程梳理

再用一個典型的例子驗證下流程:
const startTime = new Date();

setTimeout(function f1() {
  console.log('setTimeout', new Date(), new Date() - startTime);
}, 200)

console.log('node 生命周期', startTime);

const fs = require('fs')

fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) {
  const fsTime = new Date()
  console.log('fs', fsTime);
  while (new Date() - fsTime < 300) {
  }
  console.log('結束死循環', new Date());
});

連續運行三遍,打印結果如下:

如何理解Nodejs中的事件循環

執行流程解析:

  • 執行全局上下文,打印「node 生命周期 + 時間」

  • 詢問是否有event loop

  • 有,進入timers隊列,檢查沒有計時器(cpu處理速度可以,這時還沒到200ms)

  • 輪詢進入到poll,讀文件還沒讀完(比如此時才用了20ms),因此poll隊列是空的,也沒有任務回調

  • 在poll隊列等待……不斷輪詢看有沒有回調

  • 文件讀完,poll隊列有了fsFunc回調函數,并且被執行,輸出「fs + 時間」

  • 在while死循環那里卡300毫秒,

  • 死循環卡到200ms的時候,f1回調進入timers隊列。但此時poll隊列很忙,占用了線程,不會向下執行。

  • 直到300ms后poll隊列清空,輸出「結束死循環 + 時間」

  • event loop趕緊向下走

  • 再來一輪到timers,執行timers隊列里的f1回調。于是看到「setTimeout + 時間」

  • timers隊列清空,回到poll隊列,沒有任務,等待一會。

  • 等待時間夠長后,向下回到event loop。

  • event loop檢查沒有其他異步任務了,結束線程,整個程序over退出。

check 階段

檢查階段(使用 setImmediate 的回調會直接進入這個隊列)

check隊列的實際工作原理

真正的隊列,里邊扔的就是待執行的回調函數的集合。類似[fn,fn]這種形式的。
每次到達check這個隊列后,立即按順序執行回調函數即可【類似于[fn1,fn2].forEach((fn)=>fn())的感覺】

所以說,setImmediate不是一個計時器的概念。

如果你去面試,涉及到Node環節,可能會遇到下邊這個問題:setImmediate和setTimeout(0)誰更快。

setImmediate() 與 setTimeout(0) 的對比

  • setImmediate的回調是異步的,和setTimeout回調性質一致。

  • setImmediate回調在check隊列,setTimeout回調在timers隊列(概念意義,實際在計時器線程,只是setTimeout在timers隊列做檢查調用而已。詳細看timers的工作原理)。

  • setImmediate函數調用后,回調函數會立即push到check隊列,并在下次eventloop時被執行。setTimeout函數調用后,計時器線程增加一個定時器任務,下次eventloop時會在timers階段里檢查判斷定時器任務是否到達時間,到了則執行回調函數。

  • 綜上,setImmediate的運算速度比setTimeout(0)的要快,因為setTimeout還需要開計時器線程,并增加計算的開銷。

二者的效果差不多。但是執行順序不定

觀察以下代碼:

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

setImmediate(() => {
  console.log('setImmediate');
});

多次反復運行,執行效果如下:

如何理解Nodejs中的事件循環

順序不定

可以看到多次運行,兩句console.log打印的順序不定。
這是因為setTimeout的間隔數最小填1,雖然下邊代碼填了0。但實際計算機執行當1ms算。(這里注意和瀏覽器的計時器區分。在瀏覽器中,setInterval的最小間隔數為10ms,小于10ms則會被設置為10;設備供電狀態下,間隔最小為16.6ms。)

以上代碼,主線程運行的時候,setTimeout函數調用,計時器線程增加一個定時器任務。setImmediate函數調用后,其回調函數立即push到check隊列。主線程執行完畢。

eventloop判斷時,發現timers和check隊列有內容,進入異步輪詢:

第一種情況:等到了timers里這段時間,可能還沒有1ms的時間,定時器任務間隔時間的條件不成立所以timers里還沒有回調函數。繼續向下到了check隊列里,這時候setImmediate的回調函數早已等候多時,直接執行。而再下次eventloop到達timers隊列,定時器也早已成熟,才會執行setTimeout的回調任務。于是順序就是「setImmediate -> setTimeout」。

第二種情況:但也有可能到了timers階段時,超過了1ms。于是計算定時器條件成立,setTimeout的回調函數被直接執行。eventloop再向下到達check隊列執行setImmediate的回調。最終順序就是「setTimeout -> setImmediate」了。

所以,只比較這兩個函數的情況下,二者的執行順序最終結果取決于當下計算機的運行環境以及運行速度。

二者時間差距的對比代碼
------------------setTimeout測試:-------------------
let i = 0;
console.time('setTimeout');
function test() {
  if (i < 1000) {
    setTimeout(test, 0)
    i++
  } else {
    console.timeEnd('setTimeout');
  }
}
test();

------------------setImmediate測試:-------------------
let i = 0;
console.time('setImmediate');
function test() {
  if (i < 1000) {
    setImmediate(test)
    i++
  } else {
    console.timeEnd('setImmediate');
  }
}
test();

運行觀察時間差距:

如何理解Nodejs中的事件循環

setTimeout與setImmediate時間差距

可見setTimeout遠比setImmediate耗時多得多
這是因為setTimeout不僅有主代碼執行的時間消耗。還有在timers隊列里,對于計時器線程中各個定時任務的計算時間。

結合poll隊列的面試題(考察timers、poll和check的執行順序)

如果你看懂了上邊的事件循環圖,下邊這道題難不倒你!

// 說說下邊代碼的執行順序,先打印哪個?
const fs = require('fs')
fs.readFile('./poll.js', () => {
  setTimeout(() => console.log('setTimeout'), 0)
  setImmediate(() => console.log('setImmediate'))
})

上邊這種代碼邏輯,不管執行多少次,肯定都是先執行setImmediate。

如何理解Nodejs中的事件循環

先執行setImmediate

因為fs各個函數的回調是放在poll隊列的。當程序holding在poll隊列后,出現回調立即執行。
回調內執行setTimeout和setImmediate的函數后,check隊列立即增加了回調。
回調執行完畢,輪詢檢查其他隊列有內容,程序結束poll隊列的holding向下執行。
check是poll階段的緊接著的下一個。所以在向下的過程中,先執行check階段內的回調,也就是先打印setImmediate。
到下一輪循環,到達timers隊列,檢查setTimeout計時器符合條件,則定時器回調被執行。

nextTick 與 Promise

說完宏任務,接下來說下微任務

  • 二者都是「微隊列」,執行異步微任務。

  • 二者不是事件循環的一部分,程序也不會開啟額外的線程去處理相關任務。(理解:promise里發網絡請求,那是網絡請求開的網絡線程,跟Promise這個微任務沒關系)

  • 微隊列設立的目的就是讓一些任務「馬上」、「立即」優先執行。

  • nextTick與Promise比較,nextTick的級別更高。

nextTick表現形式
process.nextTick(() => {})
Promise表現形式
Promise.resolve().then(() => {})
如何參與事件循環?

事件循環中,每執行一個回調前,先按序清空一次nextTick和promise。

// 先思考下列代碼的執行順序
setImmediate(() => {
  console.log('setImmediate');
});

process.nextTick(() => {
  console.log('nextTick 1');
  process.nextTick(() => {
    console.log('nextTick 2');
  })
})

console.log('global');


Promise.resolve().then(() => {
  console.log('promise 1');
  process.nextTick(() => {
    console.log('nextTick in promise');
  })
})

最終順序:

  • global

  • nextTick 1

  • nextTick 2

  • promise 1

  • nextTick in promise

  • setImmediate

兩個問題:

基于上邊的說法,有兩個問題待思考和解決:

  • 每走一個異步宏任務隊列就查一遍nextTick和promise?還是每執行完 宏任務隊列里的一個回調函數就查一遍呢?

  • 如果在poll的holding階段,插入一個nextTick或者Promise的回調,會立即停止poll隊列的holding去執行回調嗎?

如何理解Nodejs中的事件循環

上邊兩個問題,看下邊代碼的說法

setTimeout(() => {
  console.log('setTimeout 100');
  setTimeout(() => {
    console.log('setTimeout 100 - 0');
    process.nextTick(() => {
      console.log('nextTick in setTimeout 100 - 0');
    })
  }, 0)
  setImmediate(() => {
    console.log('setImmediate in setTimeout 100');
    process.nextTick(() => {
      console.log('nextTick in setImmediate in setTimeout 100');
    })
  });
  process.nextTick(() => {
    console.log('nextTick in setTimeout100');
  })
  Promise.resolve().then(() => {
    console.log('promise in setTimeout100');
  })
}, 100)

const fs = require('fs')
fs.readFile('./1.poll.js', () => {
  console.log('poll 1');
  process.nextTick(() => {
    console.log('nextTick in poll ======');
  })
})

setTimeout(() => {
  console.log('setTimeout 0');
  process.nextTick(() => {
    console.log('nextTick in setTimeout');
  })
}, 0)

setTimeout(() => {
  console.log('setTimeout 1');
  Promise.resolve().then(() => {
    console.log('promise in setTimeout1');
  })
  process.nextTick(() => {
    console.log('nextTick in setTimeout1');
  })
}, 1)

setImmediate(() => {
  console.log('setImmediate');
  process.nextTick(() => {
    console.log('nextTick in setImmediate');
  })
});

process.nextTick(() => {
  console.log('nextTick 1');
  process.nextTick(() => {
    console.log('nextTick 2');
  })
})

console.log('global ------');

Promise.resolve().then(() => {
  console.log('promise 1');
  process.nextTick(() => {
    console.log('nextTick in promise');
  })
})

/** 執行順序如下
global ------
nextTick 1
nextTick 2
promise 1
nextTick in promise
setTimeout 0 // 解釋問題1. 沒有上邊的nextTick和promise,setTimeout和setImmediate的順序不一定,有了以后肯定是0先開始。
// 可見,執行一個隊列之前,就先檢查并執行了nextTick和promise微隊列
nextTick in setTimeout
setTimeout 1
nextTick in setTimeout1
promise in setTimeout1
setImmediate
nextTick in setImmediate
poll 1
nextTick in poll ======
setTimeout 100
nextTick in setTimeout100
promise in setTimeout100
setImmediate in setTimeout 100
nextTick in setImmediate in setTimeout 100
setTimeout 100 - 0
nextTick in setTimeout 100 - 0
 */

以上代碼執行多次,順序不變,setTimeout和setImmediate的順序都沒變。

執行順序及具體原因說明如下:

  • global :主線程同步任務,率先執行沒毛病

  • nextTick 1:執行異步宏任務之前,清空異步微任務,nextTick優先級高,先行一步

  • nextTick 2:執行完上邊這句代碼,又一個nextTick微任務,立即率先執行

  • promise 1:執行異步宏任務之前,清空異步微任務,Promise的優先級低,所以在nextTick完了以后立即執行

  • nextTick in promise:清空Promise隊列的過程中,遇到nextTick微任務,立即執行、清空

  • setTimeout 0: 解釋第一個問題. 沒有上邊的nextTick和promise,只有setTimeout和setImmediate時他倆的執行順序不一定。有了以后肯定是0先開始。可見,執行一個宏隊列之前,就先按順序檢查并執行了nextTick和promise微隊列。等微隊列全部執行完畢,setTimeout(0)的時機也成熟了,就被執行。

  • nextTick in setTimeout:執行完上邊這句代碼,又一個nextTick微任務,立即率先執行 【這種回調函數里的微任務,我不能確定是緊隨同步任務執行的;還是放到微任務隊列,等下一個宏任務執行前再清空的他們。但是順序看上去和立即執行他們一樣。不過我比較傾向于是后者:先放到微任務隊列等待,下一個宏任務執行前清空他們。】

  • setTimeout 1:因為執行微任務耗費時間,導致此時timers里判斷兩個0和1的setTimeout計時器已經結束,所以兩個setTimeout回調都已加入隊列并被執行

  • nextTick in setTimeout1:執行完上邊這句代碼,又一個nextTick微任務,立即率先執行 【可能是下一個宏任務前清空微任務】

  • promise in setTimeout1:執行完上邊這句代碼,又一個Promise微任務,立即緊隨執行 【可能是下一個宏任務前清空微任務】

  • setImmediate:poll隊列回調時機未到,先行向下到check隊列,清空隊列,立即執行setImmediate回調

  • nextTick in setImmediate:執行完上邊這句代碼,又一個nextTick微任務,立即率先執行 【可能是下一個宏任務前清空微任務】

  • poll 1:poll隊列實際成熟,回調觸發,同步任務執行。

  • nextTick in poll :執行完上邊這句代碼,又一個nextTick微任務,立即率先執行 【可能是下一個宏任務前清空微任務】

  • setTimeout 100:定時器任務到達時間,執行回調。并在回調里往微任務推入了nextTick、Promise,往宏任務的check里推入了setImmediate的回調。并且也開啟了計時器線程,往timers里增加了下一輪回調的可能。

  • nextTick in setTimeout100:宏任務向下前,率先執行定時器回調內新增的微任務-nextTick 【這里就能確定了,是下一個宏任務前清空微任務的流程】

  • promise in setTimeout100:緊接著執行定時器回調內新增的微任務-Promise 【清空完nextTick清空Promise的順序】

  • setImmediate in setTimeout 100:這次setImmediate比setTimeout(0)先執行的原因是:流程從timers向后走到check隊列,已經有了setImmediate的回調,立即執行。

  • nextTick in setImmediate in setTimeout 100:執行完上邊這句代碼,又一個nextTick微任務,下一個宏任務前率先清空微任務

  • setTimeout 100 - 0:輪詢又一次回到timers,執行100-0的回調。

  • nextTick in setTimeout 100 - 0:執行完上邊這句代碼,又一個nextTick微任務,下一個宏任務前率先清空微任務。

擴展:為什么有了setImmediate還要有nextTick和Promise?

一開始設計的時候,setImmediate充當了微隊列的作用(雖然他不是)。設計者希望執行完poll后立即執行setImmediate(當然現在也確實是這么表現的)。所以起的名字叫Immediate,表示立即的意思。 但是后來問題是,poll里可能有N個任務連續執行,在執行期間想要執行setImmediate是不可能的。因為poll隊列不停,流程不向下執行。

于是出現nextTick,真正的微隊列概念。但此時,immediate的名字被占用了,所以名字叫nextTick(下一瞬間)。事件循環期間,執行任何一個隊列之前,都要檢查他是否被清空。其次是Promise。

面試題

最后,檢驗學習成果的面試題來了

async function async1() {
  console.log('async start');
  await async2();
  console.log('async end');
}

async function async2(){
  console.log('async2');
}
console.log('script start');

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

setTimeout(() => {
  console.log('setTimeout 3');
}, 3)

setImmediate(() => {
  console.log('setImmediate');
})

process.nextTick(() => {
  console.log('nextTick');
})

async1();

new Promise((res) => {
  console.log('promise1');
  res();
  console.log('promise2');
}).then(() => {
  console.log('promise 3');
});

console.log('script end');

// 答案如下
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -
// -






/**
script start
async start
async2
promise1
promise2
script end

nextTick
async end
promise 3

// 后邊這仨的運行順序就是驗證你電腦運算速度的時候了。
速度最好(執行上邊的同步代碼 + 微任務 + 計時器運算用了不到0ms):
setImmediate
setTimeout 0
setTimeout 3

速度中等(執行上邊的同步代碼 + 微任務 + 計時器運算用了0~3ms以上):
setTimeout 0
setImmediate
setTimeout 3

速度較差(執行上邊的同步代碼 + 微任務 + 計時器運算用了3ms以上):
setTimeout 0
setTimeout 3
setImmediate
*/

思維腦圖 - Node生命周期核心階段

如何理解Nodejs中的事件循環

上述就是小編為大家分享的如何理解Nodejs中的事件循環了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

吉木乃县| 长葛市| 庆安县| 开原市| 阿鲁科尔沁旗| 元朗区| 色达县| 靖江市| 于田县| 乐业县| 岳普湖县| 沙洋县| 渝北区| 固阳县| 黄山市| 永平县| 堆龙德庆县| 五莲县| 刚察县| 长白| 清流县| 泗水县| 商水县| 济源市| 云和县| 定远县| 阜康市| 文山县| 湘潭市| 屯留县| 贵德县| 政和县| 房产| 安丘市| 宜城市| 察哈| 宁德市| 资源县| 奎屯市| 肇源县| 南雄市|