Node.js中的事件迴圈,Timers和process.nextTick() 的探索之路

shawlp發表於2019-03-29

事件迴圈

事件迴圈就是node.js去做一些非阻塞I/O操作,那麼問題來了,非阻塞操作又是什麼呢?有一個事實對於js開發者都熟知的是,js是單執行緒的,也就是說在一段時間內只能夠處理一種任務,其他任務要執行需要等待當前任務執行完之後再開始。

由於大部分的現代核心都是多執行緒的,它們能夠處理不同的操作執行。當其中的一個操作完成時,核心會告訴node,回撥函式將會被加入到執行佇列中等待被執行。

事件迴圈有好幾個階段,每個階段都有先進先出的回撥佇列被執行。每個階段都有它的特別之處,當事件迴圈進入被給予的階段時,它將會執行確切的操作,直到上一佇列已經執行完接著執行在該階段的回撥,當該階段上的所有佇列或回撥都執行完成之後,事件迴圈會轉向下一階段

階段預覽

  1. times:這個階段執行把setTimeout()和setInterval()列入計劃的回撥
  2. pending callbacks:執行I/O回撥
  3. idle, prepare: 僅為內部使用
  4. poll: 獲取新的I/O事件;執行I/O相關的回撥(大部分關閉回撥的異常)
  5. check: setImmediate()回撥將會被喚起
  6. close callbacks: 一些關閉的回撥,例如socket.on('close', ...)

times

一個timer指定了一個閾值,在一個回撥被執行之後而不是你想要讓它執行的確定的時間。timer的回撥在給定的確定時間之後將會盡可能早的執行,然而,作業系統或者其它回撥的執行可能會延遲timer的回撥

pending callbacks

這個階段執行一些系統操作的回撥,例如tcp錯誤的型別

poll

這個poll階段有兩個主要的函式:

  1. 計算它應該阻塞的時間
  2. 在該佇列中處理事件

當事件迴圈進入poll階段,並且沒有timers被安排執行,以下之一將會發生:

  1. 如果poll佇列不為空,事件迴圈將會同步迭代執行它的呼叫佇列直到佇列已經被執行完成
  2. 如果poll佇列是空的,以下之一將會發生: 如果指令碼中有setImmediate,事件迴圈將會停止poll階段,繼續轉向check階段去執行這些被安排的指令碼 若指令碼中沒有setImmediate,事件迴圈將會等待被新增進佇列的回撥,並馬上執行它們

一旦poll佇列是空的,事件迴圈將會檢查那些已經到達了設定的閾值的timers。若一個或更多的timers已經準備好,事件迴圈將會轉向執行timers階段,從而去執行這些timers的回撥函式

check

這個階段在poll階段已經完成之後允許人馬上執行回撥,如果poll階段變為了閒置狀態,且指令碼中有setImmediate,事件迴圈將會轉向check階段而不是等待。 setImmediate實際上是一個確定的timer,它執行在一個事件迴圈的獨立的階段。它使用了一個libuv api,是在poll階段完成之後,執行這些安排的回撥

close callbacks

如果一個socket或者是處理突然的被關閉了,這個close事件將會在這個階段觸發。另外,它將會通過 process.nextTick觸發。

setImmediate() VS setTimeout()

他們很類似,但是表現卻不同,這取決於他們何時被呼叫

  1. setImmediate()被設計為一旦當前的poll階段完成時,執行回撥
  2. setTimeout()是要在設定一個最小閾值的時間之後執行回撥

他們在被呼叫的不同環境下執行順序會有所不同,然而,若在I/O迴圈之內,immediate的回撥總是先執行

const fs = require('fs');

fs.readFile(__filename, () ={
  setImmediate(() ={
console.log('immediate');
  });
  setTimeout(() ={
console.log('timeout');
  }, 0);
});
複製程式碼

process.nextTick()

儘管是非同步api的一部分,但不是事件迴圈的部分。這個nextTickQueue將會在當前操作完成之後被處理,不管當前事件迴圈處在哪一階段

setImmediate() VS process.nextTick()

  1. 在同一階段process.nextTick()將會立即觸發執行
  2. setImmediate()在下一次迭代迴圈中觸發

為什麼使用process.nextTick()

  1. 允許使用者處理錯誤,在事件迴圈繼續之前清除不需要的資源或者是再次發出請求
  2. 有時候需要在執行棧結束時但要在事件迴圈繼續之前執行該回撥函式

舉個例子,如果要在函式構造器中觸發一個事件,若按照以下寫法,是不會被呼叫執行的,因為該程式碼並沒有被處理,建構函式還沒有被執行完成。

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!');
});
複製程式碼

參考資料:nodejs.org/en/docs/gui…

相關文章