Node的“事件循环”是能够处理高吞吐量场景的核心。 它是一个充满独角兽和彩虹的神奇的地方,是Node本质上可以“单线程”的原因,同时还允许在后台处理任意数量的操作。 这个帖子将揭示事件循环如何运行,这样你也可以享受魔术。

事件驱动编程

为了了解”事件循环”所需要的第一件事当然是理解事件驱动编程。事件驱动编程主要用于UI应用程序。 JavaScript的主要用法是与DOM进行交互,因此使用基于事件的API是自然的。

定义简单:事件驱动编程是由事件或状态变化决定的应用程序流控制。 一般的实现是具有侦听事件的中心机制,并且一旦检测到事件(即状态已经改变)就调用回调函数。 听起来很熟悉 这应该就是Node的事件循环的基本原理。

对于熟悉客户端JavaScript开发的人员,请考虑与DOM Elements结合使用的所有.on *()方法,如element.onclick(),以传达用户交互。 当单个项目可以发出许多可能的事件时,此模式运行良好。 Node以EventEmitter的形式使用此模式,并且位于诸如Server,Socket和“http”模块之类的位置。 当我们需要从单个实例发出多种类型的状态更改时,这很有用。

另一种常见的模式是成功或失败。 今天有两个常见的实现。 首先是“回退错误”回调风格,其中调用的错误是传递给回调的第一个参数。 ES6已经出现了第二个,使用Promises。

‘fs’模块主要使用回退回调风格。 在技术上可能会为某些调用发出额外的事件,例如fs.readFile(),但是API只是为了提醒用户,如果所需的操作成功或者某些失败。 此API选择是一种体系结构决策,而不是由于技术限制。

一个常见的误解是,事件发射器本质上是异步的,但这是不正确的。 以下是一个简单的代码片段来演示这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function MyEmitter() {
EventEmitter.call(this);
}
util.inherits(MyEmitter, EventEmitter);
MyEmitter.prototype.doStuff = function doStuff() {
console.log('before')
emitter.emit('fire')
console.log('after')}
};
var me = new MyEmitter();
me.on('fire', function() {
console.log('emit fired');
});
me.doStuff();
// Output:
// before
// emit fired
// after

EventEmitter经常出现异步,因为它经常用于表示异步操作的完成,但EventEmitter API完全是同步的。 发射功能可以异步调用,但请注意,所有侦听器函数将按照添加的顺序同步执行,之后任何执行可以在调用发出后的语句中继续执行。

机制概述

Node本身取决于多个库。 其中之一是libuv,它是处理异步事件排队和处理的魔法库。 对于这篇文章的其余部分,请记住,我不会区分一个点是否直接与Node或libuv相关。

Node尽可能利用操作系统的内核可用的内容。 因此,将制作写请求,保持连接等更多的责任由系统委托并处理。 例如,传入连接由系统排队,直到它们可以被Node处理。

您可能已经听说Node有一个线程池,可能会想知道“如果Node将所有这些责任推下来,为什么需要一个线程池? 这是因为内核不支持异步处理。 在这些情况下,Node必须在操作期间锁定线程,以便可以继续执行事件循环而不阻止。

这是一个简化的图表,用于解释何时运行的机制概览:

event loop

关于事件循环的内部工作的几个重要注意事项,将难以包含在图中:

  • 通过process.nextTick()调度的所有回调都在转换到下一阶段之前在事件循环阶段(例如定时器)结束时运行。 这会产生潜在的无意中使用循环调用process.nextTick()的事件循环。
  • “Pending callbacks”是回调排队运行,不会被任何其他阶段处理(例如,传回给fs.write()的回调)。

事件发射器(Event Emitter)和事件循环(Event Loop)

为了简化与事件循环的交互,创建了EventEmitter。 它是一个通用的包装器,可以更容易地创建基于事件的API。 由于这两种互动之间的一些困惑,我们现在将解决常常会让开发者陷入困境的问题。

以下示例显示如何忘记发生事件同步发生可能会导致用户错过事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Post v0.10, require('events').EventEmitter is not necessary.
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Sorry, never going to happen.
});

上述的缺点是,’thing1’永远不能被用户捕获,因为MyThing()必须在侦听任何事件之前完成实例化。 这是一个简单的解决方案,也不需要任何额外的关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var EventEmitter = require('events');
var util = require('util');
function MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);
function emitThing1(self) {
self.emit('thing1');
}
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// Whoot!
});

以下也可以工作,但是性能成本很高:

1
2
3
4
5
6
7
8
function MyThing() {
EventEmitter.call(this);
doFirstThing();
// Using Function#bind() makes the world much slower.
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);

另一个问题是发生错误。 解决您的应用程序的问题可能很难,但丢失调用堆栈可能会使它不可能。 当错误在异步请求的远端被实例化时,调用堆栈丢失。 解决这个问题的两个最合理的解决方案是同步发射或确保其他重要信息与错误一起传播。 以下示例显示正在使用的每个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MyThing.prototype.foo = function foo() {
// This error will be emitted asynchronously.
var er = doFirstThing();
if (er) {
// The error needs to be created immediately to preserve
// the call stack.
setImmediate(emitError, this, new Error('Bad stuff'));
return;
}
// Emit the error immediately so it can be handled.
var er = doSecondThing();
if (er) {
this.emit('error', 'More bad stuff');
return;
}
}

考虑这个情况 在应用程序执行之前,可能会立即处理正在发出的错误。 或者它可能是一个微不足道的问题,需要报告,可以很容易地处理。 此外,由于对象实例的构造可能非常不完整,因此有一个发出错误的构造函数不是一个好主意。 在这种情况下抛出异常。

-