Node.js 事件迴圈-比官方更全面

李志成發表於2019-12-27

翻譯完了之後,才發現有官方翻譯;但是本文更加全面。本文是從官方文件和多篇文章整合而來。

看完本文之後,你會發現這裡內容與《NodeJs深入淺出》第三章第四節3.4 非I/O非同步API中的內容不吻合。因為書上是有些內容是錯誤的。
還有一點的是,NodeJS的事件迴圈與Javascript的略有不同。因此需要把兩者區分開。

1. 什麼是事件迴圈(What is the Event Loop)?

事件迴圈使Node.js可以通過將操作轉移到系統核心中來執行非阻塞I/O操作(儘管JavaScript是單執行緒的)。

由於大多數現代核心都是多執行緒的,因此它們可以處理在後臺執行的多個操作。 當這些操作之一完成時,核心會告訴Node.js,以便可以將適當的回撥新增到輪詢佇列中以最終執行。 我們將在本文的後面對此進行詳細說明。

2. 這就是事件迴圈(Event Loop Explained)

Node.js啟動時,它將初始化事件迴圈,處理提供的輸入指令碼(或放入REPL,本文件未涵蓋),這些指令碼可能會進行非同步API呼叫,排程計時器或呼叫process.nextTick, 然後開始處理事件迴圈。

下圖顯示了事件迴圈操作順序的簡化概述。

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

每個階段都有一個要執行的回撥FIFO佇列。 儘管每個階段都有其自己的特殊方式,但是通常,當事件迴圈進入給定階段時,它將執行該階段特定的任何操作,然後在該階段的佇列中執行回撥,直到佇列耗盡或執行回撥的最大數量為止。 當佇列已為空或達到回撥限制時,事件迴圈將移至下一個階段,依此類推。

由於這些操作中的任何一個都可能排程更多操作,並且在poll階段處理由核心排隊的新事件(比如I/O事件),因此可以在處理poll事件時將poll事件排隊。 最終導致的結果是,長時間執行的回撥可使poll階段執行的時間比timer的閾值長得多。 有關更多詳細資訊,請參見計時器(timer)和輪詢(poll)部分。

注意:Windows和Unix / Linux實現之間存在細微差異,但這對於本演示並不重要。 最重要的部分在這裡。 實際上有七個或八個階段,但是我們關心的那些(Node.js實際使用的那些)是上面的階段。

3. 各階段概覽 Phases Overview

  • timers:此階段執行由setTimeout和setInterval設定的回撥。
  • pending callbacks:執行推遲到下一個迴圈迭代的I/O回撥。
  • idle, prepare, :僅在內部使用。
  • poll:取出新完成的I/O事件;執行與I/O相關的回撥(除了關閉回撥,計時器排程的回撥和setImmediate之外,幾乎所有這些回撥) 適當時,node將在此處阻塞。
  • check:在這裡呼叫setImmediate回撥。
  • close callbacks:一些關閉回撥,例如 socket.on('close', ...)

在每次事件迴圈執行之間,Node.js會檢查它是否正在等待任何非同步I/Otimers,如果沒有,則將其乾淨地關閉。

4. 各階段詳細解釋 Phases in Detail

4.1 timers 計時器階段

計時器可以在回撥後面指定時間閾值,但這不是我們希望其執行的確切時間。 計時器回撥將在經過指定的時間後儘早執行。 但是,作業系統排程或其他回撥的執行可能會延遲它們。-- 執行的實際時間不確定

注意:從技術上講,輪詢(poll)階段控制計時器的執行時間。

例如,假設你計劃在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
  }
});

當事件迴圈進入poll階段時,它有一個空佇列(fs.readFile尚未完成),因此它將等待直到達到最快的計時器timer閾值為止。 等待95 ms過去時,fs.readFile完成讀取檔案,並將需要10ms完成的其回撥新增到輪詢(poll)佇列並執行。 回撥完成後,佇列中不再有回撥,此時事件迴圈已達到最早計時器(timer)的閾值(100ms),然後返回到計時器(timer)階段以執行計時器的回撥。 在此示例中,您將看到計劃的計時器與執行的回撥之間的總延遲為105ms

Note: To prevent the poll phase from starving the event loop, libuv (the C library that implements the Node.js event loop and all of the asynchronous behaviors of the platform) also has a hard maximum (system dependent) before it stops polling for more events.

注意:為防止輪詢poll階段使事件迴圈陷入飢餓狀態(一直等待poll事件),libuv還具有一個硬最大值限制來停止輪詢。

4.2 pending callbacks 階段

此階段執行某些系統操作的回撥,例如TCP錯誤。 舉個例子,如果TCP套接字在嘗試連線時收到ECONNREFUSED,則某些* nix系統希望等待報告錯誤。 這將會在pending callbacks階段排隊執行。

4.3 輪詢 poll 階段

輪詢階段具有兩個主要功能:

  • 計算應該阻塞並I/O輪詢的時間
  • 處理輪詢佇列(poll queue)中的事件

當事件迴圈進入輪詢(poll)階段並且沒有任何計時器排程( timers scheduled)時,將發生以下兩種情況之一:

  • 如果輪詢佇列(poll queue)不為空,則事件迴圈將遍歷其回撥佇列,使其同步執行,直到佇列用盡或達到與系統相關的硬限制為止(到底是哪些硬限制?)。
  • 如果輪詢佇列為空,則會發生以下兩種情況之一:
    • 如果已通過setImmediate排程了指令碼,則事件迴圈將結束輪詢poll階段,並繼續執行check階段以執行那些排程的指令碼。
    • 如果指令碼並沒有setImmediate設定回撥,則事件迴圈將等待poll佇列中的回撥,然後立即執行它們。

一旦輪詢佇列(poll queue)為空,事件迴圈將檢查哪些計時器timer已經到時間。 如果一個或多個計時器timer準備就緒,則事件迴圈將返回到計時器階段,以執行這些計時器的回撥。

4.4 檢查階段 check

此階段允許在輪詢poll階段完成後立即執行回撥。 如果輪詢poll階段處於空閒,並且指令碼已使用setImmediate進入 check 佇列,則事件迴圈可能會進入check階段,而不是在poll階段等待。

setImmediate實際上是一個特殊的計時器,它在事件迴圈的單獨階段執行。 它使用libuv API,該API計劃在輪詢階段完成後執行回撥。

通常,在執行程式碼時,事件迴圈最終將到達輪詢poll階段,在該階段它將等待傳入的連線,請求等。但是,如果已使用setImmediate設定回撥並且輪詢階段變為空閒,則它將將結束並進入check階段,而不是等待輪詢事件。

4.5 close callbacks 階段

如果套接字或控制程式碼突然關閉(例如socket.destroy),則在此階段將發出'close'事件。 否則它將通過process.nextTick發出。

5. setImmediate vs setTimeout

setImmediatesetTimeout相似,但是根據呼叫時間的不同,它們的行為也不同。

  • setImmediate設計為在當前輪詢poll階段完成後執行指令碼。
  • setTimeout計劃在以毫秒為單位的最小閾值過去之後執行指令碼。

計時器的執行順序將根據呼叫它們的上下文而有所不同。 如果兩者都是主模組(main module)中呼叫的,則時序將受到程式效能的限制(這可能會受到計算機上執行的其他應用程式的影響)。有點難懂,舉個例子:

例如,如果我們執行以下不在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');
  });
});

setTimeout相比,使用setImmediate的主要優點是,如果在I/O週期內setImmediate總是比任何timers快。這個可以在下方彩色圖中找到答案:poll階段用setImmediate設定下階段check的回撥,等到了check就開始執行;timers階段只能等到下次迴圈執行!

問題:那為什麼在外部(比如主程式碼部分 mainline)這兩者的執行順序不確定呢?

解答:在mainline 部分執行setTimeout設定定時器(沒有寫入佇列呦),與setImmediate寫入check 佇列。mainline 執行完開始事件迴圈,第一階段是timers,這時候timers佇列可能為空,也可能有回撥;如果沒有那麼執行check佇列的回撥,下一輪迴圈在檢查並執行timers佇列的回撥;如果有就先執行timers的回撥,再執行check階段的回撥。因此這是timers的不確定性導致的。

舉一反三:timers 階段寫入check 佇列

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

總是會輸出:

immediate
timeout
const ITERATIONS_MAX = 2;
let iteration = 0;

const timeout = setInterval(() => {
    console.log('TIME PHASE START:' + iteration);

    if (iteration >= ITERATIONS_MAX) {
        clearInterval(timeout);
        console.log('TIME PHASE exceeded!');
    }

    console.log('TIME PHASE END:' + iteration);

    ++iteration;
}, 0);

setTimeout(() => {
    console.log('TIME PHASE0');

    setTimeout(() => {
        console.log('TIME PHASE1');

        setTimeout(() => {
            console.log('TIME PHASE2');
        });
    });
});

輸出:

TIME PHASE START:0
TIME PHASE END:0
TIME PHASE0
TIME PHASE START:1
TIME PHASE END:1
TIME PHASE1
TIME PHASE START:2
TIME PHASE exceeded!
TIME PHASE END:2
TIME PHASE2

這表明,可以理解setIntervalsetTimeout的巢狀呼叫的語法糖。setInterval(() => {}, 0)是在每一次事件迴圈中新增回撥到timers佇列。因此不會阻止事件迴圈的繼續執行,在瀏覽器上也不會感到卡頓。

6. process.nextTick

6.1 理解process.nextTick

你可能已經注意到process.nextTick並未顯示在圖中,即使它是非同步API的一部分也是如此。 這是因為process.nextTick從技術上講不是事件迴圈的一部分。 相反,無論事件迴圈的當前階段如何,都將在當前操作完成之後處理nextTickQueue。 在此,將操作定義為在C/C ++處理程式基礎下過渡並處理需要執行的JavaScript。

回顧一下我們的圖,在給定階段裡可以在任意時間呼叫process.nextTick,傳遞給process.nextTick的所有回撥都將在事件迴圈繼續之前得到解決。 這可能會導致一些不良情況,因為它允許您通過進行遞迴process.nextTick呼叫來讓I/O處於"飢餓"狀態,從而防止事件迴圈進入輪詢poll階段。

注意:Microtask callbacks 微服務

6. 2 為什麼允許這樣操作? Why would that be allowed?

為什麼這樣的東西會包含在Node.js中? 它的一部分是一種設計理念,即使不是必須的情況下,API也應始終是非同步的。

舉個例子:

function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,
                            new TypeError('argument should be string'));
}
apiCall(1, e => console.log(e));
console.log(2);
// 2
// 1

該程式碼段會進行引數檢查,如果不正確,則會將錯誤傳遞給回撥。 該API最近進行了更新,以允許將引數傳遞給process.nextTick,從而可以將回撥後傳遞的所有引數都傳播為回撥的引數,因此您不必巢狀函式。

我們正在做的是將錯誤傳遞迴使用者,但只有在我們允許其餘使用者的程式碼執行之後。 通過使用process.nextTick,我們保證apiCall始終在使用者的其餘程式碼之後以及事件迴圈繼續下階段之前執行其回撥。 為此,允許JS呼叫堆疊展開,然後立即執行所提供的回撥,該回撥可以對process.nextTick進行遞迴呼叫,而不會達到RangeErrorv8超出最大呼叫堆疊大小

這種理念可能會導致某些潛在的問題情況。 以下程式碼段為例:

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', () => {});

僅通過埠時,該埠將立即繫結。 因此,可以立即呼叫“監聽”回撥。 問題在於那時尚未設定.on('listening')回撥。

為了解決這個問題,"listening"事件在nextTick()中排隊,以允許指令碼執行完成。 這允許使用者設定他們想要的任何事件處理程式。

6.3 process.nextTick vs setImmediate

他們的呼叫方式很相似,但是名稱讓人困惑。

  • process.nextTick在同一階段立即觸發
  • setImmediate fires on the following iteration or 'tick' of the event loop(在事件迴圈接下來的階段迭代中執行 - check階段)。

本質上,名稱應互換。 process.nextTicksetImmediate觸發得更快,但由於歷史原因,不太可能改變。 進行此切換將破壞npm上很大一部分軟體包。 每天都會新增更多的新模組,這意味著我們每天都在等待,更多潛在的損壞發生。 儘管它們令人困惑,但名稱本身不會改變。

我們建議開發人員在所有情況下都使用setImmediate,因為這樣更容易推理(並且程式碼與各種環境相容,例如瀏覽器JS。)- 但是如果理解底層原理,就不一樣。

6.4 為什麼還用 process.nextTick?

這裡舉出兩個原因:

  • 在事件迴圈繼續之前下個階段允許開發者處理錯誤,清理所有不必要的資源,或者重新嘗試請求。
  • 有時需要讓回撥在事件迴圈繼續下個階段之前執行(At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.)。

簡單的例子:

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

server.listen(8080);
server.on('listening', () => { }); // 設定監聽回撥

假設listen在事件迴圈的開始處執行,但是偵聽回撥被放置在setImmediate中(實際上listen使用process.nextTick,.on在本階段完成)。 除非傳遞主機名,否則將立即繫結到埠。 為了使事件迴圈繼續進行,它必須進入輪詢poll階段,這意味著存在已經接收到連線可能性,從而導致在偵聽事件之前觸發連線事件(漏掉一些poll事件)。

另一個示例正在執行一個要從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!');
});

你無法立即從建構函式中發出事件,因為指令碼還沒執行到開發者為該事件分配回撥的那裡(指myEmitter.on)。 因此,在建構函式本身內,你可以使用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!');
});

6.5 process.nextTick 在事件迴圈的位置:

來子一位外國小哥之手。連結在本文下面。

           ┌───────────────────────────┐
        ┌─>│           timers          │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        │  │     pending callbacks     │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        |  |     idle, prepare         │
        |  └─────────────┬─────────────┘
  nextTickQueue     nextTickQueue
        |  ┌─────────────┴─────────────┐
        |  │           poll            │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        │  │           check           │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        └──┤       close callbacks     │
           └───────────────────────────┘

下圖補充了官方並沒有提及的 Microtasks微任務:

Node application lifecycle

7. Microtasks 微任務

微任務會在主線之後和事件迴圈的每個階段之後立即執行。

如果您熟悉JavaScript事件迴圈,那麼應該對微任務不陌生,這些微任務在Node中的工作方式相同。 如果你想重新瞭解事件迴圈和微任務佇列,請檢視此連結(這東西非常底層,慎點)。

在Node領域,微任務是來自以下物件的回撥:

  • process.nextTick()
  • then() handlers for resolved or rejected Promises

在主線結束後以及事件迴圈的每個階段之後,立即執行微任務回撥。

resolved的promise.then回撥像微處理一樣執行,就像process.nextTick一樣。 雖然,如果兩者都在同一個微任務佇列中,則將首先執行process.nextTick的回撥。

優先順序 process.nextTick > promise.then = queueMicrotask

下面例子完整演示了事件迴圈:

const fs = require('fs');
const logger = require('../common/logger');
const ITERATIONS_MAX = 2;
let iteration = 0;
const start = Date.now();
const msleep = (i) => {
    for (let index = 0; Date.now() - start < i; index++) {
        // do nonthing
    }
}
Promise.resolve().then(() => {
    // Microtask callback runs AFTER mainline, even though the code is here
    logger.info('Promise.resolve.then', 'MAINLINE MICROTASK');
});
logger.info('START', 'MAINLINE');
const timeout = setInterval(() => {
    logger.info('START iteration ' + iteration + ': setInterval', 'TIMERS PHASE');
    if (iteration < ITERATIONS_MAX) {
        setTimeout((iteration) => {
            logger.info('TIMER EXPIRED (from iteration ' + iteration + '): setInterval.setTimeout', 'TIMERS PHASE');
            Promise.resolve().then(() => {
                logger.info('setInterval.setTimeout.Promise.resolve.then', 'TIMERS PHASE MICROTASK');
            });
        }, 0, iteration);
        fs.readdir(__dirname, (err, files) => {
            if (err) throw err;
            logger.info('fs.readdir() callback: Directory contains: ' + files.length + ' files', 'POLL PHASE');
            queueMicrotask(() => logger.info('setInterval.fs.readdir.queueMicrotask', 'POLL PHASE MICROTASK'));
            Promise.resolve().then(() => {
                logger.info('setInterval.fs.readdir.Promise.resolve.then', 'POLL PHASE MICROTASK');
            });
        });
        setImmediate(() => {
            logger.info('setInterval.setImmediate', 'CHECK PHASE');
            Promise.resolve().then(() => {
                logger.info('setInterval.setTimeout.Promise.resolve.then', 'CHECK PHASE MICROTASK');
            });
        });
        // msleep(1000); // 等待 I/O 完成
    } else {
        logger.info('Max interval count exceeded. Goodbye.', 'TIMERS PHASE');
        clearInterval(timeout);
    }
    logger.info('END iteration ' + iteration + ': setInterval', 'TIMERS PHASE');
    iteration++;
}, 0);
logger.info('END', 'MAINLINE');

輸出:

1577168519233:INFO: MAINLINE: START
1577168519242:INFO: MAINLINE: END
1577168519243:INFO: MAINLINE MICROTASK: Promise.resolve.then

# 第一次
1577168519243:INFO: TIMERS PHASE: START iteration 0: setInterval
1577168519244:INFO: TIMERS PHASE: END iteration 0: setInterval
## 到這裡迴圈已經結束了

## 這時候 timers 階段為空, poll 階段有新事件完成
1577168519245:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files
1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask
1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then
## 在 poll 階段結束後馬上處理微任務

## poll 轉 check 階段執行 setImmediate 設定的回撥
1577168519245:INFO: CHECK PHASE: setInterval.setImmediate
1577168519245:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then

## 開始新的迴圈, timers 佇列不為空
1577168519246:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 0): setInterval.setTimeout
1577168519246:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then

# 第二次
1577168519246:INFO: TIMERS PHASE: START iteration 1: setInterval
1577168519246:INFO: TIMERS PHASE: END iteration 1: setInterval

1577168519246:INFO: CHECK PHASE: setInterval.setImmediate
1577168519246:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then

1577168519246:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files
1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask
1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then

1577168519253:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 1): setInterval.setTimeout
1577168519253:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then

# 第三次退出
1577168519253:INFO: TIMERS PHASE: START iteration 2: setInterval
1577168519253:INFO: TIMERS PHASE: Max interval count exceeded. Goodbye.
1577168519253:INFO: TIMERS PHASE: END iteration 2: setInterval

執行結果的順序不固定,因為fs.readdir需要I/O系統呼叫,需要等待系統的排程,因此等待事件並不固定。

但是順序仍然是有規律的:

  • 因為setTimeoutsetImmediatetimers階段(不是mainline就行)被呼叫,因此setImmediate總是比setTimeout快(前面第5節已說明)
  • 因為poll階段等待系統呼叫的時間不確定。因此它會在上面兩者之間插空,就是3種排序
    • poll check timers 這種可能比較少,取決於I/O呼叫速度與程式在當前timers階段的處理時間——也就是I/O的事件迴圈進入poll階段前就已經完成,也就是poll佇列不為空。把上面的msleep註釋開啟即可測試。
    • check poll timers 這種情況比較多出現。
    • check timers poll 這種情況也多。

因此存在3種順序。

本文下方連結包含更多例子

timers階段和poll階段,因為依賴系統的排程,所以具體在哪一次事件迴圈執行?這是不確定的,有可能是下次迴圈就可以,也許需要等待。在上面彩色圖的事件迴圈中黃色標記的階段中,只剩下check階段是確定的 —— 必然是在本次(還沒到本次迴圈的check階段的話)或者下次迴圈呼叫。還有的是, 微服務是能夠保證,必然在本階段結束後下階段前執行。

timers 不確定,poll 不確定,check 確定,Microtasks確定。

8. 題外話:Events

事件是應用程式中發生的重要事件。 諸如Node之類的事件驅動的執行時在某些地方發出事件,並在其他地方響應事件。

例子:

// The Node EventEmitter
const EventEmitter = require('events');
// Create an instance of EventEmitter
const eventEmitter = new EventEmitter();

// The common logger
const logger = require('../common/logger');

logger.info('START', 'MAINLINE');

logger.info('Registering simpleEvent handler', 'MAINLINE');
eventEmitter.on('simpleEvent', (eventName, message, source, timestamp) => {
logger.info('Received event: ' + timestamp + ': ' + source + ':[' + eventName + ']: ' + message, 'EventEmitter.on()');
});

// Get the current time
let hrtime = process.hrtime();
eventEmitter.emit('simpleEvent', 'simpleEvent', 'Custom event says what?', 'MAINLINE', (hrtime[0] * 1e9 + hrtime[1] ) / 1e6);

logger.info('END', 'MAINLINE');

輸出:

$ node example7
1530379926998:INFO: MAINLINE: START
1530379927000:INFO: MAINLINE: Registering simpleEvent handler
1530379927000:INFO: EventEmitter.on(): Received event: 553491474.966337: MAINLINE:[simpleEvent]: Custom event says what?
1530379927000:INFO: MAINLINE: END

上面結果看出, Event是同步, 什麼時候emit 就什麼時候執行回撥。

這些資料是通過必應國際版搜尋出來,百度不給力。

原文官方解釋

Phases of the Node JS Event Loop

Learn Node.js, Unit 5: The event loop 其他章節:Learn Nodejs

Node Events

本作品採用《CC 協議》,轉載必須註明作者和本文連結

有什麼想法歡迎提問或者資訊

相關文章