第一次翻译文档,渣翻请见谅。
原文链接 https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
参考链接 http://www.cnblogs.com/MuYunyun/p/7287413.html
http://www.cnblogs.com/MuYunyun/p/7287413.html
什么是事件循环?
事件循环是使Node.js得以执行非阻塞I/O操作的——即使JavaScript是单线程的,通过在任何可能的时候将操作offload到系统内核。
最现代的内核是多线程的,它们能处理多个操作在后台执行。当这些操作的其中之一完成,内核告诉Node.js,使得合适的callback可以被加入到poll队列中,最终被执行。
事件循环解释
当Node.js启动,它初始化事件循环,处理提供的输入脚本(或丢入REPL),这可能导致异步API调用,调度定时器,或者调用process.nextTick(),然后开始处理事件循环。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections,┤
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
每个阶段有一个callback的FIFO队列等待执行。一般来说,当事件循环进入到一个给定的阶段,它会执行这个阶段的所有具体的操作,然后执行这个阶段的队列中的callback,直到队列好近或callback 的最大数量被执行。之后事件循环会移动到下一个阶段。
阶段概览
- timers: 计时器,这个阶段执行通过setTimeout()和setInterval()注册的回调函数。
- pending callbacks: 执行被推迟到下个循环迭代的I/O回调函数,大部分回调将在这里被处理
- idle, prepare: 只在内部使用
- poll: 轮询,对接着要处理的I/O事件进行新的轮询,执行与I/O相关的回调函数(几乎所有,除了close callbacks,这个通过即时起注册,以及setImmediate())
- check: setImmediate()回调函数在这里被调用
-
close callbacks: 一些关闭回调函数,如socket.on('close', ...), 处理所有‘结束’事件的回调。
在每轮事件循环期间,Node.js检查是否在等待任何异步I/O或计时器,如果没有,就完整关闭。
阶段细节
timers
一个定时器指定阈值,一个提供的回调函数可能在这个threshold阈值之后,而不是我们想要它被执行的exact实际的时间被执行。定时器的回调函数会在它们被指派在指定长度的时间之后尽早执行;然而,操作系统调度或其他回调函数的运行可能会推迟它们。
注意:理论上,exact轮询阶段控制定时器何时被执行。
例如,你设置一个timeout在100ms的阈值吼执行,然后你的搅拌开始异步读取一个文件,读取文件花费95ms:
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()
尚未完成),因此它会等待剩余的毫秒数直到达到最快定时器的阈值。当等待了95ms,fs.readFile()
完成读取文件,它的花费10ms来完成的回调函数被加入到poll队列并被执行。当回调函数完成,队列中没有其他的回调函数了,于是事件循环会看最快定时器的阈值已经被达到,然后wrap back回到timers定时器阶段来执行定时器的回调函数。在这个例子中,你会看到定时器被设置和它的回调函数被执行的总延迟为105ms。
注意: 为了避免poll轮询阶段耗尽事件循环,libuv(实现Node.js事件循环和平台的所有异步行为的C库)也有一个hard最大值(系统依赖)来阻止对更多事件轮询。
pending callbacks
这个阶段为一些系统操作,如TCP错误类型,执行回调函数。例如,如果一个TCP套接字在尝试连接时接收到了ECONNREFUSED
,一些系统想等待来报告这个错误。这回被放入pending callbacks的队列中来执行。
poll
poll轮询阶段有两个主要的功能:
- 计算它该阻塞多久和轮询I/O, 然后
- 处理poll队列中的事件。
当事件循环进入poll阶段,且没有定时器被设置,两件事之一会发生:
- 如果poll阶段的队列不为空,则事件循环会遍历回调函数的队列,同步执行它们,直到队列为空或达到系统依赖的hard limit。
- 如果poll阶段的队列为空,则以下两件事之一会发生:
- 如果脚本已经被
setImmediate()
设置,事件循环会终止poll阶段并继续到check阶段来执行那些被设置的脚本。 - 如果脚本尚未被
setImmediate()
设置,事件循环会等待回调函数被加入到队列中,然后立刻执行它们
- 如果脚本已经被
一旦poll队列为空,事件循环会检查timers定时器,它们的时间阈值被达到。如果一个或多个定时器就绪,事件循环会回到timers阶段来执行那些定时器的回调函数。
check
这个阶段允许我们在poll阶段完成后立即执行回调函数。如果poll阶段变为闲置,且脚本被setImmediate()
设置,事件循环会来到check阶段而不是等待。
setImmediate()
实际是一个特殊的定时器,运行在事件循环的分开的阶段。它使用一个libuvAPI,这个API设置回调函数在poll阶段完成后来执行。
一般来说,在代码被执行时,事件循环最终会进入poll阶段,在这里它会等待一个到来的连接,请求,等等。然而,如果一个回调函数已经被setImmediate()
设置且poll阶段变为闲置,它会终止并进入到check阶段而不是等待poll轮询事件。
close callbacks
如果一个socket或handle被突然关闭(e.g. socket.destroy()
),这个'close'
事件会在这个阶段被射出。否则它会通过process.nextTick()
被射出。
setImmediate()
VS setTimeout()
两者类似,但是根据被调用的时间会发生不同的行为。
-
setImmediate()
被设计为一旦现在的poll阶段完成就执行。 -
setTimeout()
设置一个脚本在一个最小毫米单位阈值过去之后执行。
定时器被执行的顺序会根据它们被调用时处在的上下文而变化。如果都在主模块中被调用,则timing会被进程的性能限制(进程的性能可能会被机器上运行的其他应用程序影响)。
例如,如果我们运行以下的不处于一个I/O cycle(也就是主模块)内部的脚本,这两个定时器被执行的顺序是不一定的,因为受到了进程性能的限制:
// 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 cycle里面,immediate 回调函数总是先执行:
// 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()
的主要优点是,当处在I/O cycle之内时,前者总是在任何其他定时器之前执行,不管当前有多少个定时器。