为什么setImmediate()在Nodejs事件循环中的fs.readFile()之前执行?

时间:2017-12-09 02:43:36

标签: javascript node.js event-loop

我已经阅读了很多相关文件。但我仍然无法理解它是如何运作的。

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()仍然先运行。

2 个答案:

答案 0 :(得分:11)

您看到的行为是因为事件循环中有多种类型的队列,系统根据其类型按顺序运行事件。它不仅仅是一个巨型事件队列,其中所有内容都以FIFO顺序运行,具体取决于它何时被添加到事件队列中。相反,它喜欢运行一种类型的所有事件(最多限制),前进到下一种类型,运行所有这些等等。

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

  

然后我认为事件循环将转移到定时器(无),I / O回调(fs.readFile&#39; s回调),空闲/准备(无),轮询(无),检查(setImmediate&#39; s callback)并最终按顺序关闭回调(none),但结果是setImmediate()仍然先运行。

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

但是,setImmediate()回调在第一次通过事件循环时已准备就绪,因此它会在readFile()回调之前运行。

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

以下是while循环结束时会发生什么:

  1. 当while循环结束时,它以计时器回调开始,并看到计时器已准备好运行,因此它运行了。
  2. 然后,它会运行已经存在的任何I / O回调,但还没有。尚未收集readFile()的I / O回调。它将在本周期后期收集。
  3. 然后,它经历了几个其他阶段并进入I / O轮询。收集readFile()回调事件并将其放入I / O队列(但尚未运行)。
  4. 然后,它进入checkHandlers阶段,运行setImmediate()回调。
  5. 然后,它再次启动事件循环。没有计时器,所以它进入I / O回调,它最终找到并运行readFile()回调。
  6. 因此,让那些不熟悉事件循环过程的人更详细地记录代码中实际发生的事情。当您运行此代码时(将时间添加到输出中):

    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
    

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

    以下是发生的事情:

    1. 计时器启动并设置为10ms,其他代码继续运行
    2. fs.readFile()操作已启动,其他代码继续运行
    3. setImmediate()已注册到事件系统中,其事件位于相应的事件队列中,其他代码继续运行
    4. while循环开始循环
    5. while循环期间,fs.readFile()完成其工作(在后台运行)。它的事件已准备就绪,但尚未进入相应的事件队列(稍后会详细介绍)
    6. while循环在循环1秒后完成,这个Javascript的初始序列完成并返回系统
    7. 口译员现在需要得到&#34; next&#34;来自事件循环的事件。但是,所有类型的事件都没有得到平等对待。事件系统具有特定的顺序,它处理队列中不同类型的事件。在我们的例子中,这里首先处理计时器事件(我将在下面的文中解释)。系统检查是否有任何计时器已经过期&#34;并准备调用他们的回调。在这种情况下,它发现我们的计时器已经过期&#34;并准备好了。
    8. 调用定时器回调,我们看到控制台消息timer
    9. 没有更多的计时器,因此事件循环进入下一阶段。事件循环的下一个阶段是运行任何挂起的I / O回调。但是,事件队列中还没有挂起的I / O回调。即使readFile()现在已经完成,它还没有进入队列(解释即将到来)。
    10. 然后,下一步是收集已完成的任何I / O事件并使其准备好运行。在这里,将收集readFile()事件(虽然尚未运行)并将其放入I / O事件队列。
    11. 然后下一步是运行任何待处理的setImmediate()处理程序。当它这样做时,我们得到输出immediate
    12. 然后,事件过程的下一步是运行任何关闭处理程序(这里没有运行)。
    13. 然后,通过检查定时器重新开始事件循环。没有待定的计时器可以运行。
    14. 然后,事件循环运行任何挂起的I / O回调。这里readFile()回调运行,我们在控制台中看到readfile
    15. 程序没有其他等待的事件,因此执行。
    16. 事件循环本身是一系列用于不同类型事件的队列(有一些例外),每个队列在进入下一类队列之前都会被处理。这会导致事件分组(一个组中的计时器,另一个组中的待处理I / O回调,另一个组中的setImmediate(),依此类推)。它不是所有类型中的严格FIFO队列。事件是组内的FIFO。但是,所有挂起的计时器回调(达到某种限制以使一种类型的事件无限期地阻止事件循环)在其他类型的回调之前被处理。

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

      enter image description here

      来自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
      

      解释类似(这次缩短了一点,因为之前解释了很多细节)。

      1. setImmediate()已注册到相应的队列中。
      2. setTimeout()已注册到计时器队列中。
      3. while循环运行1000ms
      4. 代码完成执行并将控制权返回给系统
      5. 系统从事件逻辑的顶部开始,该逻辑以计时器事件开始。我们之前启动的计时器现在已完成,因此它会运行其回调并记录timer
      6. 如果没有更多的计时器,事件循环将运行其他几种类型的事件队列,直到它到达运行setImmediate()处理程序的位置并记录immediate
      7. 如果您有多个项目计划在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
        

        在上面的流程图中,您可以看到&#34;运行完成的I / O处理程序&#34;步骤是。因为将在I / O处理程序中调度setTimeout()setImmediate()调用,所以它们将在&#34;运行已完成的I / O处理程序&#34;事件循环的阶段。按照事件循环的流程,setImmediate()将在&#34;检查处理程序&#34;中得到服务。在事件循环回到服务定时器之前的阶段。

        如果setImmediate()setTimeout()被安排在事件循环中的不同点,则计时器可能会在setImmediate()之前触发,这是前面示例中发生的情况。因此,两者的相对时序取决于调用函数时事件循环的相位。

答案 1 :(得分:0)

setTimeout(() => console.log('timer'), 10);                
fs.readFile(__filename, () => console.log('readfile'));    
setImmediate(() => console.log('immediate'));               

while(Date.now() - now < 1000) {
}

<强>解释

  1. {10}之后将setTimeout计划放入事件循环中。

  2. 异步文件读取开始。

  3. 非标setImmediate个时间表,用于显示控制台输出中断长进程。

  4. 运行一秒钟的阻塞循环。控制台中没有任何内容。

  5. setImmediate在循环期间输出immediate控制台消息。

  6. 文件读取结束,即使在while循环之后也会执行回调 过度。控制台输出readfile现在就在那里。

  7. 最后,大约10秒后打印出控制台消息timer

  8. 注意事项

    • 以上命令(循环除外)均不是同步的。他们 安排一些事情,然后立即进入下一个命令。

    • 仅在当前阻止后才调用回调函数 执行结束了。

    • 不保证在指定的时间执行超时命令 间隔。保证是他们会在随时运行 间隔。

    • setImmediate非常具有实验性。