欢迎来到我们关于可视化Node.js
事件循环的系列文章的第五篇。在上一篇文章中,我们探讨了I/O
队列及其在执行异步代码时的优先级顺序。在本文中,我们将继续专注于I/O
队列,并逐渐引入检查队列。有一个重要的观点需要注意,我将在我们的下一个实验中解释。
回调函数队列
在我们继续实验之前,我想提一下,为了将回调函数入队到检查队列中,我们使用内置的setImmediate()
函数。语法很简单:setImmediate(callbackFn)
。当这个函数在调用栈上执行时,回调函数将被入队到检查队列中。
前八个实验涉及微任务、计时器和
I/O
队列,并已在之前的文章中介绍过。所有实验都是使用CommonJS
模块格式运行的。
实验 9
1 | // index.js |
代码片段延续了前一个实验。它包括对readFile()
的调用,将回调函数排入I/O
队列,对process.nextTick()
的调用,将回调函数排入nextTick
队列,对Promise.resolve().then()
的调用,将回调函数排入promise
队列,以及对setTimeout()
的调用,将回调函数排入计时器队列。
在这个实验中引入的setImmediate()
调用将回调函数排入检查队列。为了避免实验7中的计时器问题,一个长时间运行的for
循环确保当控制进入计时器队列时,setTimeout()
计时器已经过去,回调准备好被执行。
如果运行代码片段,您可能会注意到输出不是您所期望的。来自setImmediate()
的回调消息在readFile()
的回调消息之前被记录。这里是参考输出。
这可能看起来有些奇怪,因为I/O
队列出现在检查队列之前,但一旦我们理解了在这两个队列之间发生的I/O
轮询的概念,就会变得合理。为了帮助说明这个概念,让我提供一个可视化。
首先,所有函数都在调用栈上执行,导致回调函数排队到相应的队列中。然而,readFile()
的回调函数并没有同时排队。让我解释一下为什么。
当控制进入事件循环时,首先检查微任务队列中的回调函数。在这种情况下,nextTick
队列和promise
队列中各有一个回调函数。nextTick
队列具有优先级,因此我们首先看到"nextTick 1"
被记录,然后是"Promise 1"
。
两个队列都是空的,控制权移动到计时器队列。有一个回调函数,将"setTimeout 1"
记录到控制台。
现在来看有趣的部分。当控制进入I/O
队列时,我们期望readFile()
的回调函数会出现,对吗?毕竟,我们有一个长时间运行的for
循环,而且readFile()
应该已经完成了。
然而,在现实中,事件循环必须进行轮询以检查I/O
操作是否已完成,它只会将已完成操作的回调排队。这意味着当控制第一次进入I/O
队列时,队列仍然为空。
然后,控制权移动到事件循环的轮询部分,在那里它询问readFile()
任务是否已完成。readFile()
确认已完成,并且事件循环现在将关联的回调函数添加到I/O
队列中。然而,执行已经超过了I/O
队列,回调函数必须等待轮到它被执行。
然后,控制进入检查队列,在那里找到一个回调函数。它将"setImmediate 1"
记录到控制台,然后开始一个新的迭代,因为在事件循环的当前迭代中没有其他要处理的内容了。
看起来微任务队列和计时器队列都是空的,但是I/O
队列中有一个回调函数。执行了这个回调函数,最终将"readFile 1"
记录到控制台。
这就是为什么我们在"setImmediate 1"
之前看到"readFile 1"
被记录的原因。这种行为实际上在我们之前的实验中也发生过,但是我们没有进一步的代码来运行,所以我们没有观察到它。
只有在I/O完成后,I/O事件才会被轮询,并且回调函数会被添加到I/O队列中。
结论
一旦I/O
操作完成,其回调函数不会立即排队到I/O
队列中。相反,I/O
轮询阶段检查I/O
操作的完成情况,并将已完成操作的回调函数排队。这有时会导致在I/O队列回调之前执行检查队列回调。
然而,当两个队列都包含回调函数时,I/O
队列中的回调函数始终具有优先级并且会先运行。当设计依赖于I/O
回调的系统时,理解这种行为对于确保回调的正确顺序和执行至关重要。