node nextTick属于idle阶段吗?

1,642 阅读4分钟

1.背景

最近在看朴灵老师的node深入浅出,注意到一句

process.nextTick()中的回调函数执行的优先级要高于setImmediate().这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一轮循环检查中,idle观察者优先于I/O观察者, I/O观察者优先于check观察者

开始我对这段话不太在意,对嘛应该就是这样。然后再去看下官方文档以及掘金的一些相关文章浏览器与Node的事件循环(Event Loop)有何区别?发现了这样一张图

从上图中,大致看出node中的事件循环的顺序: 外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

咦? 怎么冲突了呀,根据上图中的描述 idle阶段是晚于 I/O Callbacks 阶段的。 查询了官方文档后,关于process.nextTick()的介绍

This is because process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop

大概意思就是 process.nextTick()根本就不是eventloop的一部分,但是在eventloop的每个阶段结束后都会去将队列中的process.nextTick任务执行了。 有张图可以描述eventloop阶段和nextTickQueue之间的关系。

所以按照这样来说 深入浅出中关于nextTick的描述其实是不太准确的。

2.解释上图

上图是来自某篇文章 www.zcfy.cc/article/nod… 里面对一些概念做了解释。我再结合深入浅出里面的知识点加一些备注

定时器(Timer)阶段

这个是事件循环开始的阶段,绑定到这个阶段的队列,保留着定时器(setTimeout, setInterval)的回调。尽管它并没有将回调推入队列中,但是以最小的堆来维持计时器并且在到达规定的时间后执行回调。

tip:

调用setTimeout()或者setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中

这里我觉得最小堆可能更加合适,找队列中最紧急的任务。 孰优孰劣可以自行查找下 最小堆 vs 红黑树

悬而未决的(Pending) i/o 回调阶段

这个阶段执行在事件循环中 pending_queue 里的回调。这些回调是被之前的操作推入的。例如当你尝试往 tcp 中写入一些东西,这个工作完成了,然后回调被推入到队列中。错误处理的回调也在这里

Idle, Prepare 阶段

尽管名字是空闲(idle),但是每个 tick 都运行。Prepare 也在轮询阶段开始之前运行。不管怎样,这两个阶段是 node 主要做一些内部操作的阶段;因此,我们不在这儿讨论。今天不讨论。

轮询(Poll)阶段

可能整个事件循环最重要的一个阶段就是 poll phase。这个阶段接受新传入的连接(新的 Socket 建立等)和数据(文件读取等)(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外)。我们可以将轮询阶段分成几个不同的部分。

如果在 watch_queue(这个队列被绑定到轮询阶段)有东西,它们将会被一个接着一个的执行直到队列为空或者系统到达最大的限制。 一旦队列为空,node 就会等待新的连接。等待或者睡眠的时间取决于多种因素,待会儿我们会讨论。

检查(Check)阶段

轮询的下一个阶段是 check phase,这个专用于 setImmediate 的阶段。为什么需要一个专门的队列来处理 setImmediate 回调。这是因为轮询阶段的行为,待会儿将在流程部分讨论。现在只需要记住检查(check)阶段主要用于处理 setImmediate() 的回调。

关闭(Close)回调

回调的关闭(socket.on(‘close’, ()=>{})) 都在这里处理的,更像一个清理阶段

nextTickQueue & microTaskQueue

nextTickQueue 中的任务保留着被 process.nextTick() 触发的回调。microTaskQueue 保留着被 Promise 触发的回调。它们都不是事件循环的一部分(不是在 libUV 中开发的),而是在 node.js 中。在 C/C++ 和 Javascript 有交叉的时候,它们都是尽可能快地被调用。因此它们应该在当前操作运行后(不一定是当前 js 回调执行完)。