Node.js 事件循环-比官方更全面

Node 事件循环

翻译完了之后,才发现有官方翻译;但是本文更加全面。本文是从官方文档和多篇文章整合而来。

看完本文之后,你会发现这里内容与《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 协议》,转载必须注明作者和本文链接

有什么想法欢迎提问或者资讯

讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!