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
循环结束时发生的情况:
- 当while循环结束时,它以计时器callback开始,并看到计时器已准备好运行,所以它运行。
- 然后,它会运行任何已经存在的I / Ocallback,但是还没有。
readFile()
的I / Ocallback还没有被收集。 它将在这个周期的晚些时候收集。 - 然后,通过其他几个阶段进行I / O调查。 有收集
readFile()
callback事件,并将其放入I / O队列(但不运行它)。 - 然后,它进入checkHandlers阶段,运行
setImmediate()
callback。 - 然后,它再次启动事件循环。 没有定时器,所以它进入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
我已经添加了相对于程序启动的时间,所以你可以看到什么时候执行。
以下是发生的事情:
- 定时器从现在开始并设置为10ms,其他代码继续运行
-
fs.readFile()
操作启动,其他代码继续运行 -
setImmediate()
被注册到事件系统中,并且它的事件在适当的事件队列中,其他代码继续运行 -
while
循环开始循环 - 在
while
循环期间,fs.readFile()
完成了它的工作(在后台运行)。 事件已经准备就绪,但尚未进入适当的事件队列(稍后会有更多内容) -
while
循环while
循环结束1秒后完成,并且这个Javascript的初始序列完成并返回到系统 - 解释器现在需要从事件循环中获取“下一个”事件。 但是,所有types的事件都没有同等对待。 事件系统具有处理队列中不同types事件的特定顺序。 在我们的例子中,在这里,定时器事件首先被处理(我将在下面的文字中解释这一点)。 系统检查是否有任何计时器已经“过期”,并准备拨打他们的回叫。 在这种情况下,它发现我们的计时器已经“过期”并准备好了。
- 定时器callback被调用,我们看到控制台消息
timer
。 - 没有更多的定时器,事件循环进入下一个阶段。 事件循环的下一个阶段是运行任何未决的I / Ocallback。 但是,事件队列中还没有挂起的I / Ocallback。 即使
readFile()
现在已经完成,它仍然不在队列中(解释即将到来)。 - 然后,下一步是收集已完成的任何I / O事件,并让它们准备好运行。 在这里,
readFile()
事件将被收集(尽pipe还没有运行)并放入I / O事件队列中。 - 然后下一步是运行任何待处理的
setImmediate()
处理程序。 当它这样做,我们得到immediate
产出。 - 然后,事件进程的下一步是运行任何closures处理程序(这里没有任何运行)。
- 然后,通过检查定时器重新开始事件循环。 没有待定的计时器可以运行。
- 然后,事件循环运行任何悬而未决的I / Ocallback。 这里
readFile()
callback运行,我们看到控制台中的readfile
。 - 程序没有更多的事件等待执行。
事件循环本身是针对不同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
解释是相似的(这一次缩短了一些,因为很多细节在前面解释过)。
-
setImmediate()
被注册到适当的队列中。 -
setTimeout()
被注册到定时器队列中。 - while循环运行了1000ms
- 代码完成执行并将控制权返还给系统
- 系统从以定时器事件开始的事件逻辑开始。 我们之前启动的定时器现在已经完成,所以它会运行它的callback和日志
timer
。 - 没有更多的定时器,事件循环会经过几个其他types的事件队列,直到它运行到
setImmediate()
处理程序所在的位置并immediate
logging。
setTimeout(() => console.log('timer'), 10); fs.readFile(__filename, () => console.log('readfile')); setImmediate(() => console.log('immediate')); while(Date.now() - now < 1000) { }
说明
-
setTimeout
时间表在10ms后放入一个事件循环中。 -
asynchronous文件读取开始。
-
非标准的
setImmediate
调度显示一个控制台输出中断长进程。 -
一秒钟的阻塞循环运行。 没有在控制台呢。
-
setImmediate
在循环中打印immediate
控制台消息。 -
即使在
while
循环结束之后,文件读取结束并且callback也被执行。 控制台输出readfile
现在在那里。 -
最后,控制台消息
timer
在大约10秒钟后打印。
注意事项
-
以上命令(循环除外)都不是同步的。 他们安排了一些东西,立即进入下一个命令。
-
callback函数只有在当前阻塞执行结束后才会被调用。
-
超时命令不保证以指定的时间间隔执行。 保证是他们将在任何时间间隔后运行。
-
setImmediate
是非常实验性的。