我已经阅读了很多相关的文件。但我仍然无法理解它是如何运作的。
const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
const now = Date.now();
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timer'), 10);
while(Date.now() - now < 1000) {
}
我认为第一段代码应该记录:
readfile
immediate
第二段代码记录。
timer
immediate
我觉得还可以。
问题:我不明白为什么第一段代码会记录下来
immediate
readfile
我认为该文件已被完全读取,其回调函数在1秒后将I / O回调阶段的队列中的队列进行排队。
然后我认为事件循环将按顺序移动到timers(none)
,I/O callbacks(fs.readFile's callback)
,idle/prepare(none)
,poll(none)
,check(setImmediate's callback)
and和close callbacks(none)
,但结果是setImmediate()
仍然首先运行。
您看到的行为是因为事件循环中有多种类型的队列,系统根据其类型按顺序运行事件。它不仅仅是一个巨型事件队列,其中所有内容都以FIFO顺序运行,具体取决于它何时被添加到事件队列中。相反,它喜欢运行一种类型的所有事件(最多限制),前进到下一种类型,运行所有这些等等。
并且,I / O事件仅在循环中的某个特定点添加到其队列中,因此它们被强制为特定顺序。这就是setImmediate()
回调在readFile()
回调之前执行的原因,即使在while
循环完成时两者都准备好了。
然后我认为事件循环将转移到定时器(无),I / O回调(fs.readFile的回调),空闲/准备(无),轮询(无),检查(setImmediate的回调)和最后关闭回调(无)顺序,但结果是setImmediate()仍然先运行。
问题是事件循环的I / O回调阶段运行已经在事件队列中的I / O回调,但是当它们完成时它们不会自动进入事件队列。相反,它们仅在I/O poll
步骤的过程中稍后放入事件队列中(参见下图)。因此,第一次通过I / O回调阶段,还没有运行I / O回调,因此当你认为你没有得到readfile
输出。
但是,setImmediate()
回调已经准备好第一次通过事件循环,所以它在readFile()
回调之前运行。
这延迟添加I / O回调可能解释了为什么你感到惊讶的是readFile()
回调发生在最后而不是在setImmediate()
回调之前。
以下是while
循环结束时发生的情况:
readFile()
的I / O回调尚未收集。它将在此周期的后期收集。readFile()
回调事件并将其放入I / O队列(但尚未运行)。setImmediate()
回调。readFile()
回调。因此,让我们更详细地记录代码中实际发生的事情,以便那些不熟悉事件循环过程的人。当您运行此代码时(将时间添加到输出):
const fs = require('fs')
let begin = 0;
function log(msg) {
if (!begin) {
begin = Date.now();
}
let t = ((Date.now() - begin) / 1000).toFixed(3);
console.log("" + t + ": " + msg);
}
log('start program');
setTimeout(() => log('timer'), 10);
setImmediate(() => log('immediate'));
fs.readFile(__filename, () => log('readfile'));
const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');
你得到这个输出:
0.000: start program
0.004: start loop
1.004: done loop
1.005: timer
1.006: immediate
1.008: readfile
我已经添加了相对于程序启动时间的秒数,这样你就可以看到事情的执行时间。
这是发生的事情:
fs.readFile()
操作启动,其他代码继续运行setImmediate()
已注册到事件系统中,其事件位于相应的事件队列中,其他代码继续运行while
循环开始循环while
循环期间,fs.readFile()
完成其工作(在后台运行)。它的事件已准备就绪,但尚未在相应的事件队列中(稍后会详细介绍)while
循环在循环1秒后完成,这个Javascript的初始序列完成并返回到系统timer
。readFile()
现在已经完成,它还没有进入队列(解释即将到来)。readFile()
事件(虽然尚未运行)并将其放入I / O事件队列中。setImmediate()
处理程序。当它这样做时,我们得到输出immediate
。readFile()
回调运行,我们在控制台中看到readfile
。事件循环本身是一系列用于不同类型事件的队列(有一些例外),每个队列在进入下一类队列之前都会被处理。这会导致事件分组(一组中的计时器,另一组中的待处理I / O回调,另一组中的setImmediate()
等)。它不是所有类型中的严格FIFO队列。事件是组内的FIFO。但是,所有挂起的计时器回调(达到某种限制以使一种类型的事件无限期地阻止事件循环)在其他类型的回调之前被处理。
您可以在此图中看到基本结构:
来自this very excellent article。如果您真的想了解所有这些内容,请多次阅读此引用文章。
最初让我感到惊讶的是为什么readFile
总是在最后。这是因为即使完成了readFile()
操作,也不会立即将其放入队列中。相反,在事件循环中有一个步骤,其中收集完成的I / O事件(将在下一个循环中通过事件循环进行处理),并且在当前循环结束之前处理setImmediate()
事件,然后执行I / O事件。刚收集。这使得readFile()
回调在setImmediate()
回调之后,即使它们都准备好在while循环期间进行。
此外,执行readFile()
和setImmediate()
的顺序无关紧要。因为它们都准备好在while
循环完成之前完成,所以它们的执行顺序是通过事件循环的顺序确定的,因为它运行不同类型的事件,而不是它们完成的时间。
在你的第二个代码块中,你删除readFile()
并将setImmediate()
放在setTimeout()
之前。使用我的定时版本,就是这样:
const fs = require('fs')
let begin = 0;
function log(msg) {
if (!begin) {
begin = Date.now();
}
let t = ((Date.now() - begin) / 1000).toFixed(3);
console.log("" + t + ": " + msg);
}
log('start program');
setImmediate(() => log('immediate'));
setTimeout(() => log('timer'), 10);
const now = Date.now();
log('start loop');
while(Date.now() - now < 1000) {}
log('done loop');
并且,它生成此输出:
0.000: start program
0.003: start loop
1.003: done loop
1.005: timer
1.008: immediate
解释是类似的(这次缩短了一点,因为之前解释了很多细节)。
setImmediate()
已注册到适当的队列中。setTimeout()
已注册到计时器队列中。timer
。setImmediate()
处理程序的位置,并记录immediate
。如果,您有多个项目计划在I / O回调中启动,例如:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
然后,您会得到略微不同的行为,因为当事件循环处于其循环的不同部分时,将调度setTimeout()
和setImmediate()
。在这个特定的例子中,setImmediate()
将始终在计时器之前执行,因此输出将是:
immediate
timeout
在上面的流程图中,您可以看到“运行已完成的I / O处理程序”步骤的位置。因为setTimeout()
和setImmediate()
调用将在I / O处理程序中进行调度,所以它们将在事件循环的“运行完成的I / O处理程序”阶段进行调度。按照事件循环的流程,setImmediate()
将在事件循环回到服务定时器之前的“检查处理程序”阶段得到服务。
如果setImmediate()
和setTimeout()
被安排在事件循环中的不同点,那么计时器可能会在setImmediate()
之前触发,这是前面示例中发生的情况。因此,两者的相对时序取决于调用函数时事件循环的相位。
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
说明
setTimeout
计划在10ms后进入事件循环。setImmediate
计划显示控制台输出中断了长进程。setImmediate
在循环期间打印immediate
控制台消息。while
循环结束后也会执行回调。控制台输出readfile
现在在那里。timer
。注意事项
setImmediate
非常实验性。