NodeJs 事件循環-比官方更全面

Node 事件循環

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

看完本文以後,你會發現這裏內容與《NodeJs深刻淺出》第三章第四節3.4 非I/O異步API中的內容不吻合。由於書上是有些內容是錯誤的。html

1. 什麼是事件循環(What is the Event Loop)?

事件循環使Node.js能夠經過將操做轉移到系統內核中來執行非阻塞I/O操做(儘管JavaScript是單線程的)。java

因爲大多數現代內核都是多線程的,所以它們能夠處理在後臺執行的多個操做。 當這些操做之一完成時,內核會告訴Node.js,以即可以將適當的回調添加到輪詢隊列中以最終執行。 咱們將在本文的後面對此進行詳細說明。node

2. 這就是事件循環(Event Loop Explained)

Node.js啓動時,它將初始化事件循環,處理提供的輸入腳本(或放入REPL,本文檔未涵蓋),這些腳本可能會進行異步API調用,調度計時器或調用process.nextTick, 而後開始處理事件循環。web

下圖顯示了事件循環操做順序的簡化概述。npm

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
每一個階段都有一個要執行的回調FIFO隊列。 儘管每一個階段都有其本身的特殊方式,可是一般,當事件循環進入給定階段時,它將執行該階段特定的任何操做,而後在該階段的隊列中執行回調,直到隊列耗盡或執行回調的最大數量爲止。 當隊列已爲空或達到回調限制時,事件循環將移至下一個階段,依此類推。

因爲這些操做中的任何一個均可能調度更多操做,而且在poll階段處理由內核排隊的新事件(好比I/O事件),所以能夠在處理poll事件時將poll事件排隊。 最終致使的結果是,長時間運行的回調可以使poll階段運行的時間比timer的閾值長得多。 有關更多詳細信息,請參見計時器(timer)和輪詢(poll)部分。api

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

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

相關文章
相關標籤/搜索