我已经阅读了很多相关文档。但是我仍然不明白它是如何工作的。

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)和最终close callbacks(none),但是结果是setImmediate()仍然首先运行。

最佳答案

您看到的行为是因为事件循环中存在多种队列,并且系统根据事件的类型按顺序运行事件。这不仅仅是一个巨大的事件队列,所有事件都基于添加到事件队列的时间以FIFO顺序运行。相反,它喜欢运行一种类型的事件(最大),前进到下一种类型,运行所有这些事件,依此类推。

而且,I/O事件仅在循环中的某个特定点添加到其队列中,因此它们被强制为特定顺序。这就是setImmediate()回调在readFile()回调之前执行的原因,即使当while循环完成时两者都准备就绪。



问题是事件循环的I/O回调阶段运行已在事件队列中的I/O回调,但是完成后它们不会自动放入事件队列中。相反,它们只会在I/O poll步骤的过程中稍后放入事件队列中(请参见下图)。因此,第一次进入I/O回调阶段,尚无I/O回调运行,因此您以为自己不会获得readfile输出。

但是,setImmediate()回调在事件循环中第一次准备就绪,因此可以在readFile()回调之前运行。

I/O回调的这种延迟添加很可能解释了为什么您对readFile()回调发生在最后而不是在setImmediate()回调之前感到惊讶。

这是while循环结束时发生的情况:

  • 当while循环完成时,它以计时器回调开始,并看到计时器已准备好运行,因此可以运行它。
  • 然后,它将运行已经存在的所有I/O回调,但是还没有。尚未从readFile()回调I/O。它将在此周期的后期收集。
  • 然后,它经历了其他几个阶段,并进行了I/O轮询。这里收集了readFile()回调事件,并将其放入I/O队列中(但尚未运行)。
  • 然后,它进入checkHandlers阶段,在其中运行setImmediate()回调。
  • 然后,它再次开始事件循环。没有计时器,因此它转到I/O回调,最后找到并运行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
    

    我添加了相对于程序启动时间的时间(以秒为单位),以便您可以看到何时执行程序。

    这是发生了什么:
  • 计时器已启动并从现在开始设置10ms,其他代码继续运行
  • fs.readFile()操作已启动,其他代码继续运行
  • setImmediate()已注册到事件系统中,并且其事件在相应的事件队列中,其他代码继续运行
  • while循环开始循环
  • while循环中,fs.readFile()完成其工作(在后台运行)。它的事件已经准备好,但是尚未在适当的事件队列中(稍后会详细介绍)
  • 循环1秒钟后,
  • while循环完成,此Javascript的初始序列已完成,并返回系统
  • 解释器现在需要从事件循环中获取“下一个”事件。但是,所有类型的事件都不会得到同等对待。事件系统具有处理队列中不同类型事件的特定顺序。在我们的例子中,计时器事件首先得到处理(我将在下文中对此进行解释)。系统检查是否有任何计时器已“到期”并准备调用其回调。在这种情况下,它发现我们的计时器已“到期”并且可以使用了。
  • 调用了计时器回调,我们看到控制台消息timer
  • 不再有计时器,因此事件循环进入下一阶段。事件循环的下一个阶段是运行所有未决的I/O回调。但是,事件队列中还没有待处理的I/O回调。即使现在完成了readFile(),它仍未在队列中(即将进行解释)。
  • 然后,下一步是收集所有已完成的I/O事件并使它们准备好运行。在这里,readFile()事件将被收集(尽管尚未运行)并放入I/O事件队列。
  • 然后,下一步是运行所有暂挂的setImmediate()处理程序。完成此操作后,我们将获得输出immediate
  • 然后,事件过程的下一步是运行任何关闭处理程序(这里没有要运行的处理程序)。
  • 然后,事件循环通过检查计时器重新开始。没有待运行的计时器。
  • 然后,事件循环运行所有未决的I/O回调。在此运行readFile()回调,我们会在控制台中看到readfile
  • 该程序没有其他要等待的事件,因此它可以执行。

  • 事件循环本身是一系列用于不同类型事件的队列,并且(有一些异常(exception)),每个队列在移入下一个类型的队列之前都经过处理。这将导致事件分组(一组中的计时器,另一组中的未决I/O回调,另一组中的setImmediate()等)。在所有类型中,它都不是严格的FIFO队列。事件是组中的FIFO。但是,在所有其他类型的回调之前,将处理所有待处理的计时器回调(达到某种限制以防止一种类型的事件无限期地拖延事件循环)。

    您可以在此图中看到基本结构:

    javascript - 为什么在Nodejs Event Loop的fs.readFile()之前先执行setImmediate()?-LMLPHP

    来自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()已注册到计时器队列中。
  • while循环运行其1000毫秒
  • 代码完成执行并将控制权返回给系统
  • 系统从事件逻辑的顶部开始,该事件以计时器事件开始。我们之前启动的计时器现已完成,因此它将运行其回调并记录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()之前触发,这在前面的示例中会发生。因此,两者的相对计时取决于调用函数时事件循环所处的阶段。

    10-04 22:07