(译)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!');
});
复制代码
相关文章
相关标签/搜索