NodeJs事件循环中的setTimeout()如何工作?

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

但我仍然不明白在Nodejs事件循环的阶段setTimeout()如何?

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) { } 

我认为第一块代码应该logging:

 readfile immediate 

而第二块代码日志。

 timer immediate 

我认为没关系。

问题:我不明白为什么第一块代码是日志

 immediate readfile 

我认为文件已被完全读取,其callback函数在1秒后入队I / Ocallback阶段的队列。

然后我认为事件循环将移动到timers(none)I/O callbacks(fs.readFile's callback)idle/prepare(none)poll(none)check(setImmediate's callback) ,最后close callbacks(none)顺序,但结果是setImmediate()仍然先运行。

您看到的行为是因为在事件循环中存在多种types的队列,并且系统根据其types按顺序运行事件。 这不仅仅是一个巨大的事件队列,所有的事件都是按照先进先出的顺序运行的,这是基于它被添加到事件队列的时间。 相反,它喜欢运行所有types的事件(达到极限),前进到下一个types,运行所有这些等等。

而且,I / O事件只在周期中的一个特定点添加到它们的队列中,所以它们被强制为一个特定的顺序。 这就是setImmediate()callback在readFile()callback之前执行的原因,即使在while循环完成时都准备好了。

然后我认为事件循环将移动到定时器(无),I / Ocallback(fs.readFile的callback),空闲/准备(无),民意调查(无),检查(setImmediate的callback),最后closurescallback(无)顺序,但结果是setImmediate()仍然先运行。

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

但是, setImmediate()callback是第一次通过事件循环就绪,因此它在readFile()callback之前运行。

I / Ocallback的延迟添加很可能解释了为什么您会惊讶于readFile()callback发生在最后而不是setImmediate()callback之前。

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

  1. 当while循环结束时,它以计时器callback开始,并看到计时器已准备好运行,所以它运行。
  2. 然后,它会运行任何已经存在的I / Ocallback,但是还没有。 readFile()的I / Ocallback还没有被收集。 它将在这个周期的晚些时候收集。
  3. 然后,通过其他几个阶段进行I / O调查。 有收集readFile()callback事件,并将其放入I / O队列(但不运行它)。
  4. 然后,它进入checkHandlers阶段,运行setImmediate()callback。
  5. 然后,它再次启动事件循环。 没有定时器,所以它进入I / Ocallback,它终于find并运行readFile()callback。

因此,让我们详细logging下代码中实际发生的事情,以及那些不熟悉事件循环过程的人员。 当你运行这个代码(添加到输出的时间):

 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循环while循环结束1秒后完成,并且这个Javascript的初始序列完成并返回到系统
  7. 解释器现在需要从事件循环中获取“下一个”事件。 但是,所有types的事件都没有同等对待。 事件系统具有处理队列中不同types事件的特定顺序。 在我们的例子中,在这里,定时器事件首先被处理(我将在下面的文字中解释这一点)。 系统检查是否有任何计时器已经“过期”,并准备拨打他们的回叫。 在这种情况下,它发现我们的计时器已经“过期”并准备好了。
  8. 定时器callback被调用,我们看到控制台消息timer
  9. 没有更多的定时器,事件循环进入下一个阶段。 事件循环的下一个阶段是运行任何未决的I / Ocallback。 但是,事件队列中还没有挂起的I / Ocallback。 即使readFile()现在已经完成,它仍然不在队列中(解释即将到来)。
  10. 然后,下一步是收集已完成的任何I / O事件,并让它们准备好运行。 在这里, readFile()事件将被收集(尽pipe还没有运行)并放入I / O事件队列中。
  11. 然后下一步是运行任何待处理的setImmediate()处理程序。 当它这样做,我们得到immediate产出。
  12. 然后,事件进程的下一步是运行任何closures处理程序(这里没有任何运行)。
  13. 然后,通过检查定时器重新开始事件循环。 没有待定的计时器可以运行。
  14. 然后,事件循环运行任何悬而未决的I / Ocallback。 这里readFile()callback运行,我们看到控制台中的readfile
  15. 程序没有更多的事件等待执行。

事件循环本身是针对不同types的事件的一系列队列,并且(有一些例外),在移动到下一types的队列之前处理每个队列。 这会导致事件(一个组中的定时器,另一个组中的I / Ocallback,另一个组中的setImmediate()等)的分组。 这不是所有types中严格的先进先出队列。 事件在一个组内是FIFO。 但是,在其他types的callback之前处理所有未决的定时器callback(达到某种限制以保持一种types的事件无限期地处理事件循环)。

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

在这里输入图像描述

这来自这个非常优秀的文章 。 如果你真的想了解所有这些东西,那么读这个参考文章几次。

最初让我感到惊讶的是为什么readFile总是在最后。 这是因为即使readFile()操作完成,也不会立即放入队列中。 相反,在收集完成的I / O事件的事件循环中有一个步骤(将在事件循环的下一个循环中处理),并在I / O事件之前的当前周期结束时处理setImmediate()事件那只是收集。 这使得readFile()callback在setImmediate()callback之后进行,即使它们都在while循环期间准备好了。

此外,执行readFile()setImmediate()顺序并不重要。 因为它们都准备好在while循环完成之前去执行,所以它们的执行顺序是由事件循环顺序决定的,因为事件循环运行的是不同types的事件,而不是完成时间。


在第二个代码块中,删除readFile()并在setTimeout()之前放置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. 系统从以定时器事件开始的事件逻辑开始。 我们之前启动的定时器现在已经完成,所以它会运行它的callback和日志timer
  6. 没有更多的定时器,事件循环会经过几个其他types的事件队列,直到它运行到setImmediate()处理程序所在的位置并immediatelogging。
 setTimeout(() => console.log('timer'), 10); fs.readFile(__filename, () => console.log('readfile')); setImmediate(() => console.log('immediate')); while(Date.now() - now < 1000) { } 

说明

  1. setTimeout时间表在10ms后放入一个事件循环中。

  2. asynchronous文件读取开始。

  3. 非标准的setImmediate调度显示一个控制台输出中断长进程。

  4. 一秒钟的阻塞循环运行。 没有在控制台呢。

  5. setImmediate在循环中打印immediate控制台消息。

  6. 即使在while循环结束之后,文件读取结束并且callback也被执行。 控制台输出readfile现在在那里。

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

注意事项

  • 以上命令(循环除外)都不是同步的。 他们安排了一些东西,立即进入下一个命令。

  • callback函数只有在当前阻塞执行结束后才会被调用。

  • 超时命令不保证以指定的时间间隔执行。 保证是他们将在任何时间间隔运行。

  • setImmediate是非常实验性的。