什麼是事件迴圈?詳解Node.js中的事件迴圈

2022-03-25 22:00:22
什麼是事件迴圈?本篇文章給大家介紹一下中的事件迴圈,希望對大家有所幫助!

什麼是事件迴圈?

儘管JavaScript是單執行緒的,但是事件迴圈儘可能的使用系統核心允許執行非阻塞I/O操作 儘管大部分現代核心是多執行緒的,他們可以在後臺處理多執行緒任務。當一個任務完成時,核心告訴Node.js,然後適當的回撥會被加入到迴圈中執行,這篇文章會進一步詳細的介紹這個話題

時間迴圈解釋

當Node.js開始執行時,首先會初始化事件迴圈,處理提供的輸入指令碼(或者放入REPL,本檔案未涉及)這會執行非同步 API呼叫,排程計時器,或呼叫 process.nextTick(),然後開始處理事件迴圈

下圖展示了事件迴圈執行順序的簡化概覽

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

每一個盒子代表著事件迴圈的一個階段

每一個階段有一個FIFO的佇列 callback 執行,然而每一個階段基於它自己的方式執行,總體來講,當事件迴圈進入到一個階段裡,它將執行當前階段的任何操作,開始執行當前階段佇列中的回撥直到佇列完全消耗完或者執行到佇列的最巨量資料。當佇列消耗完或者達到最大數量,事件迴圈就會移動到下一個階段。

階段概述

  • timers 這個階段執行 setTimeout() 和 setInterval() 的回撥
  • pending callbacks 執行 I/O 回撥推遲到下一個迴圈迭代
  • idle,prepare 僅在內部使用
  • poll 檢索新的 I/O 事件;執行 I/O 相關的回撥(幾乎所有相關的回撥,關閉回撥,)
  • check setImmediate() 會在此階段呼叫
  • close callbacks 關閉回撥,例如: socket.on('close', ...)

在事件迴圈的每個過程中,Node.js檢查是否它正在等待非同步的I/O和計時器,如果沒有則完全關閉

階段詳情

timer

一個計時器指定一個回撥會被執行的臨界點,而不是人們想讓它執行的時間,計時器會在指定的過去時間之後儘可能早的執行,然而,作業系統排程或者其他回撥會讓它延遲執行。

從技術角度上講,poll 階段決定了回撥何時執行

例如,你設定了一個計時器,100 ms之後執行,然而你的指令碼非同步讀取了一個檔案花費了 95ms

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

當事件迴圈進入了 poll 階段,是一個空的佇列,(fs.readFile() 還沒有完成),因此它會等待剩餘的毫秒數直到最快的計時器閾值到達,當95 ms之後,fs.readFile() 完成了讀檔案並且會花費10 ms完成新增到poll 階段並且執行完畢,當回撥完成,佇列中沒有回撥要執行了,事件迴圈迴圈返回到timers 階段,執行計時器的回撥。在這個例子中,你會看到計時器被延遲了105 ms之後執行

為了防止 poll 階段阻塞事件迴圈,libuv(實現了事件迴圈和平臺上所有的非同步行為的C語言庫)在 poll 階段同樣也有一個最大值停止輪訓更多事件

pending callbacks

此階段為某些系統操作(例如 TCP 錯誤型別)執行回撥。 例如,如果 TCP 通訊端在嘗試連線時收到 ECONNREFUSED,則某些 *nix 系統希望等待報告錯誤。 這將在掛起的回撥階段排隊執行。

poll

poll 階段有兩個主要的功能

  1. 計算 I/O 阻塞的時間
  2. 執行 poll 佇列中的事件

當事件迴圈進入到了poll階段並且沒有計時器,發生以下兩種事情

  • 如果 poll 佇列中不為空,事件迴圈會同步地迭代執行每個回撥直到執行所有,或者達到系統的的硬限制
  • 如果 poll 佇列是空的,以下兩種情況會發生
    • 如果是setImmediate的回撥,事件迴圈會結束 poll 階段並進入到 check 階段執行回撥
    • 如果不是setImmediate,事件迴圈會等待回撥新增到佇列中,然後立即執行

一旦 poll 佇列是空的,事件迴圈會檢測計時器是否到時間,如果有,事件迴圈會到達timers 階段執行計時器回撥

check

此階段允許人們在 poll 階段完成後立即執行回撥。 如果輪詢階段變得空閒並且指令碼已使用 setImmediate() 排隊,則事件迴圈可能會繼續到 check 階段而不是等待。

setImmediate() 實際上是一個特殊的計時器,它在事件迴圈的單獨階段執行。 它使用一個 libuv API 來安排在 poll 階段完成後執行的回撥。

通常,隨著程式碼的執行,事件迴圈最終會到達 poll 階段,它將等待傳入的連線、請求等。但是,如果使用 setImmediate() 安排了回撥並且 poll 階段變得空閒,它將結束並繼續 check 階段,而不是等待 poll 事件。

close callbacks

如果一個 socket 或者操作突然被關閉(e.g socket.destroy()),close 事件會被傳送到這個階段,否則會通過process.nextTick()傳送

setImmediate() VS setTimeout()

setImmediate() 和 setTimeout() 是相似的,但是不同的行為取決於在什麼時候被呼叫

  • setTimmediate() 在 poll 階段一旦執行完就會執行
  • setTimeout() 在一小段時間過去之後被執行

每個回撥執行的順序依賴他們被呼叫的上下本環境,如果在同一個模組被同時呼叫,那麼時間會受到程序效能的限制(這也會被執行在這臺機器的其他應用所影響)

例如,如果我們不在I/O裡邊執行下面的指令碼,儘管它受程序效能的影響,但是不能夠確定這兩個計時器的執行順序:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

然而,如果你移動到I/O 迴圈中,immediate 回撥總是會先執行

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

setImmediate 相對於 setTimeout 的優勢是 setImmediate 如果在I/O 中總是會優先於任何計時器被先執行,與存在多少計時器無關。

process.nextTick()

儘管 process.nextTick() 是非同步API的一部分,但是你可能已經注意到了它沒有出現在圖表中,這是因為 process.nextTick() 不是事件迴圈技術的一部分,相反,當前操作執行完畢之後 nextTickQueue 會被執行,無論事件迴圈的當前階段如何。 在這裡,操作被定義為來自底層 C/C++ 處理程式的轉換,並處理需要執行的 JavaScript。 根據圖表,你可以在任意階段呼叫 process.nextTick(),在事件迴圈繼續執行之前,所有傳遞給 process.nextTick() 的回撥都將被執行,這個會導致一些壞的情況因為它允許你遞迴呼叫 process.nextTick() "starve" 你的 I/O ,這會阻止事件迴圈進入 poll 階段。

為什麼這會被允許

為什麼這種情況會被包含在Node.js中?因為Node.js的設計理念是一個API應該總是非同步的即使它不必須,看看下面的片段

function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(
      callback,
      new TypeError('argument should be string')
    );
}

該片段會進行引數檢查,如果不正確,它會將錯誤傳遞給回撥。 API 最近更新,允許將引數傳遞給 process.nextTick() 允許它接受在回撥之後傳遞的任何引數作為回撥的引數傳播,因此您不必巢狀函數。

我們正在做的是將錯誤傳回給使用者,但前提是我們允許使用者的其餘程式碼執行。 通過使用 process.nextTick(),我們保證 apiCall() 總是在使用者程式碼的其餘部分之後和允許事件迴圈繼續之前執行它的回撥。 為了實現這一點,允許 JS 呼叫堆疊展開,然後立即執行提供的回撥,這允許人們對 process.nextTick() 進行遞迴呼叫,而不會達到 RangeError:從 v8 開始超出最大呼叫堆疊大小。

更多node相關知識,請存取:!

以上就是什麼是事件迴圈?詳解Node.js中的事件迴圈的詳細內容,更多請關注TW511.COM其它相關文章!