NodeJs-06 Event Loop
Event Loop
Event Loop 可以让 Node.js 实现非阻塞的 IO 操作,尽管 Node.js 本身是单线程的。
Node.js 通过将自身的操作卸载到操作系统中,现在的操作系统大多数是多核的,
可以再后台处理多个操作,并且当操作完成之后,会在合适的时间通知 Node.js,让 Node.js 拿到真正的执行结果。
Event Loop 解释
Node.js 启动时,开始初始化 Event Loop,用来执行输入的脚本。可以调用异步 Api,定时 Timers 或者执行 process.nextTick(),
然后开始执行 Event Loop。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │ {
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 {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});- 测试 & 结果
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout执行顺序-在 IO 循环
如果二者都在 IO 循环中,immediate callback 永远首先被执行。(这也正是优点)
// 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
timeoutprocess.nextTick()
理解
您可能已经注意到,尽管它是异步API的一部分,但在图中没有显示 process.nextTick()。
这是因为 process.nextTick() 在技术上不是事件循环的一部分。
相反,不管事件循环的当前阶段如何,nextTickQueue 将在当前操作完成后进行处理。
回头看我们的图表,任何时候调用 process.nextTick() 在给定的阶段,所有回调都传递给 process.nextTick() 将在事件循环继续之前得到解决。
这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick() 调用来“饿死”您的I/O,从而阻止事件循环到达 poll 阶段。
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'));
}该代码段执行一个参数检查,如果它不正确,它将把错误传递给回调。
最近更新的API允许将参数传递给 process.nextTick(),允许它在回调后传递的任何参数作为回调的参数,这样就不必嵌套函数了。
我们所做的是将错误传递给用户,但只有在我们允许用户代码的其余部分执行之后。
通过使用 process.nextTick(),我们保证 apiCall()总是在用户代码的其余部分和事件循环允许进行之前运行它的回调。
为了实现这一点,JS调用堆栈允许unwind,然后立即执行所提供的回调,
允许一个人在没有到达RangeError的情况下对 process.nextTick()进行递归调用。
潜在问题
这种理念(philosophy)会导致一些潜在的问题。以这个片段为例:
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()中排队,以允许脚本运行到完成。这允许用户设置他们想要的任何事件处理程序。
process.nextTick() vs setImmediate()
process.nextTick() fires immediately on the same phase
setImmediate() fires on the following iteration or 'tick' of the event loop
本质上,名称应该被交换。nextTick() 比setImmediate()更容易触发,但这是过去的一个工件,不太可能发生变化。
做这个开关会在npm上破坏大量的软件包。每天都有更多的新模块被添加进来,这意味着我们每天都在等待,更多潜在的破坏发生。
虽然他们很困惑,但名字本身不会改变。
我们建议开发人员在所有情况下使用setImmediate(),因为它更容易推理(而且它会导致代码与更广泛的环境兼容,比如浏览器JS)。
Why use process.nextTick()?
主要2个原因:
允许用户处理错误,清除任何不必要的资源,或者在事件循环继续之前尝试再次请求。
有时,有必要允许回调在调用堆栈解除后运行,但在事件循环继续之前。
符合用户期望的例子
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });listen()是在事件循环的开头运行的,但是监听回调被放置在setImmediate()中。
除非通过了主机名,否则将立即绑定到端口。对于要进行的事件循环,它必须击中轮询阶段,这意味着有一个非零的机会,
连接可以被接收,允许连接事件在监听事件之前被触发。
另一个例子
另一个示例是运行一个函数构造函数,继承自 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!');
});