(譯)The Node.js Event Loop, Timers and process.nextTick()

什麼是Event loop

Event Loop使Node.js能夠作非阻塞 I/O操做,儘管實際上JavaScript是單線程的 -- 儘量的經過下發操做給操做系統內核。 大部分現代內核是多線程支持的,它們能夠在後臺處理多個操做。當這些操做中有一個完成時,內核通知Node.js執行已經被添加到了 poll 隊列中對應的回調函數。咱們會在以後更多的討論這方面的細節。node

Event Loop Explained

當Node.js開始執行,它初始化了Event loop,執行輸入提供的腳本里可能使用異步api、定時器或者調用process.nextTick(),這時就開始處理Event loop。 下面的圖表簡單展現了event loop執行順序概況。 npm

注意:每個塊都被當作一個event loop階段

每個階段都有一個FIFO隊列來執行回調。一般,當event loop進度到一個給定的階段時,每一個階段都有特定處理的事情,它將執行特定於該階段的任何操做,而後再該階段的隊列中執行回調,直到隊列爲空或到執行最大回調數。當隊列耗盡或者回調數到達限制,event loop將移至下一階段,以此類推。 因爲任何這些操做均可以安排更多操做,而且在loop階段處理的新事件由內核排隊,輪詢事件能夠在處理輪詢事件時加入隊列。所以,長時間執行的回調能使poll階段執行的比timer的閾值長的多。在timers和poll段落有更多的描述。 ###階段概覽api

  • timers: 這個階段執行setTimeout和setInterval預先設置的回調函數
  • pending callbacks: 執行延遲到下一次循環迭代的 I/O 回調函數
  • idle,prepare: 只在內部使用
  • poll:取到新的I/O事件,執行I/O先關的回調(除了close callbacks和由定時器或setImmediate調度之外的幾乎全部回調),在這裏將會在適當的時候阻塞。
  • check:setImmediate()回調函數會在這裏執行。
  • close callbacks: 一些close callbacks,例如:socket.on('close',...) . 在每次event loop執行間隔,Node.js檢查是否在等待的異步I/O或定時器,若是沒有就關閉循環。

###階段詳情 ####定時器 定時器有指定的閾值,在閾值以後能夠執行提供的回調,而不是你想要執行它的確切時間。定時器回調在指定的時間過去後會盡早的被安排執行;可是,操做系統調度或其餘回調的容許可能會延遲它們。瀏覽器

注意:嚴格來講,poll階段控制何時執行timers。多線程

例如,假設您計劃在100毫秒閾值後執行超時,那麼您的腳本將異步讀取一個耗時95毫秒的文件:異步

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
  }
});
複製代碼

當event loop進入到poll階段,存在一個空的隊列(fs.readFile()尚未完成),因此它會逗留幾毫秒直到下一個最近的定時器到達閾值。在這裏等待了95ms,fs.readFile()完成文件讀取,接着會消耗10ms把對應的回調添加到poll隊列並執行。當回調執行完成,poll隊列中沒有其餘回調函數,因此event loop會查看若是有達到時間最近定時器的閾值,就回到計時器階段以執行計時器的回調。在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將爲105毫秒。 ###pending callbacks 此階段執行某些系統操做(例如TCP錯誤類型)的回調。例如,若是TCP套接字在嘗試鏈接時收到ECONNREFUSED,在某些*nix系統但願等待報告錯誤。這將排隊等待在掛起的pending回調階段執行。 ###poll poll階段有兩個主要功能:socket

  • 計算它應該阻止和輪詢I / O的時間,而後
  • 處理輪詢隊列中的事件 當event loop進入到poll階段而且沒有定時器時,將執行如下兩個分支之一:
  • 若是poll隊列不爲空,則事件循環將遍歷其同步執行它們的回調隊列,直到隊列已用盡或者達到系統相關的硬限制。
  • 若是poll隊列爲空,將執行如下兩個分支之一:
    • 若是setImmediate()已調度腳本,則事件循環將結束輪詢階段並繼續執行檢查階段以執行這些調度腳本。
    • 若是setImmediate()還沒有調度腳本,則事件循環將等待將回調添加到隊列,而後當即執行它們。

輪詢隊列爲空後,事件循環將檢查已達到時間閾值的計時器。若是一個或多個計時器準備就緒,事件循環將回繞到計時器階段以執行那些計時器的回調。async

check

此階段容許在輪詢階段完成後當即執行回調。若是輪詢階段變爲空閒而且腳本已使用setImmediate()排隊,則事件循環能夠繼續到檢查階段而不是等待。函數

setImmediate()其實是一個特殊的計時器,它在event loop的一個單獨階段運行。它使用libuv API來調度在輪詢階段完成後執行的回調。oop

一般,在執行代碼時,事件循環最終會到達輪詢階段,它將等待傳入鏈接,請求等。可是,若是已使用setImmediate()調度回調而且輪詢階段變爲空閒,則將結束並繼續進入檢查階段,而不是一直等待poll events。

close callbacks

若是一個socket或者handle忽然關閉(例如:socket.destroy()),在這個階段會觸發 ‘close’事件,不然它將經過process.nextTick()觸發。

setImmediate() VS setTimeout()

setImmediate和setTimeout()相似,但在不一樣的調用場景有不一樣的運行方式。

  • setImmediate()設計用於在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 循環內調用,setImmediate 老是被優先調用:

// 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()

您可能已經注意到 process.nextTick() 在關係圖中沒有顯示,即便它是異步 API 的一部分。這是由於 process.nextTick() 在技術上不是事件循環的一部分。相反,不管事件循環的當前階段如何,都將在當前操做完成後處理 nextTickQueue。 回顧咱們的關係圖,在給定的階段中任什麼時候候您調用 process.nextTick()時,全部傳遞到 process.nextTick() 的回調將在事件循環繼續以前獲得解決。這可能會形成一些糟糕的狀況, 由於它容許您經過進行遞歸 process.nextTick() 來「餓死」您的 I/O 調用,阻止事件循環到達 輪詢 階段。

爲何會容許這樣?

爲何這樣的事情會包含在 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的最大調用堆棧大小。 這種理念可能會致使一些潛在的問題。以此片斷爲例:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined
});

bar = 1;
複製代碼

用戶將someAsyncApiCall()定義爲具備異步簽名,但它其實是同步操做的。調用它時,在事件循環的同一階段調用提供給someAsyncApiCall()的回調,由於someAsyncApiCall()實際上不會異步執行任何操做。所以,回調嘗試引用bar,即便它在範圍內可能沒有該變量,由於該腳本沒法運行完成。

經過將回調放在process.nextTick()中,腳本仍然可以運行完成,容許在調用回調以前初始化全部變量,函數等。它還具備不容許事件循環繼續的優勢。在容許事件循環繼續以前,向用戶警告錯誤多是有用的。如下是使用process.nextTick()的前一個示例:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined
});

bar = 1;
複製代碼

用戶將 someAsyncApiCall() 定義爲具備異步簽名,但實際上它是同步運行的。當調用它時,提供給 someAsyncApiCall() 的回調在同一階段調用事件循環,由於 someAsyncApiCall() 實際上並無異步執行任何事情。所以,回調嘗試引用 bar,在它做用域範圍內可能尚未該變量,由於腳本沒法運行到完成。

經過將回調置於 process.nextTick() 中,腳本仍具備運行完成的能力,容許在調用回調以前初始化全部變量、函數等。它還具備不容許事件循環繼續的優勢。在容許事件循環繼續以前,對用戶發出錯誤警報可能頗有用。下面是使用 process.nextTick() 的上一個示例:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;
複製代碼

這有是另一個真實的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});
複製代碼

僅當傳遞端口時,端口當即綁定。所以,能夠當即調用'listen'回調。問題是那時候不會設置.on('listen')回調。 爲了解決這個問題,'listen'事件在nextTick()中排隊,以容許腳本運行完成。這容許用戶設置他們想要的任何事件處理程序。

process.nextTick() vs setImmediate()

就用戶而言,咱們有兩個相似的呼叫,但它們的名稱使人困惑。

  • process.nextTick() 在同一階段當即觸發。
  • 在下一次迭代或事件循環的「tick」時觸發

本質上,二者的名字須要交換。process.nextTick() 比 setImmediate()觸發的更「當即」,但過去的設計不太可能改變。進行這個切換會破壞npm上的大部分包。天天都會添加更多新模塊,這意味着咱們天天都在等待,更多的潛在破損發生。雖然它們使人困惑,但名稱自己不會改變。

咱們建議開發人員在全部狀況下都使用setImmediate(),由於它更容易推理(而且它致使代碼與更普遍的環境兼容,如瀏覽器JS。)

爲何使用process.nextTick()?

有兩個主要緣由:

  • 容許用戶處理錯誤,清除任何不須要的資源,或者在事件循環繼續以前再次嘗試請求。
  • 有時須要容許回調在調用堆棧展開以後但在事件循環繼續以前運行。

一個例子是匹配用戶的指望。簡單的例子:

const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });
複製代碼

假設listen()在事件循環開始時運行,可是監聽回調放在setImmediate()中。除非傳遞hostname,不然將當即綁定到端口。要是事件循環繼續,它必須達到poll階段,這意味着存在一個非零機率已經先收到連接,會在listening 事件前觸發connection 事件。 另外一個例子是運行一個函數構造函數,好比繼承自EventEmitter,它想在構造函數中調用一個事件:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
複製代碼

您沒法當即從構造函數中發出事件,由於腳本將不會處理到用戶爲該事件分配回調的位置.所以,在構造函數自己中,您可使用process.nextTick()設置回調以在構造函數完成後發出事件,從而提供預期的結果:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
複製代碼
相關文章
相關標籤/搜索