JavaScript 是一门和事件循环结合非常紧密的语言,它最早出现在浏览器上,因为 JavaScript 为了保持 DOM 与操作相分离,同时协调浏览器上的各种资源,因而不得不有一个总线去处理,这个总线就是事件循环。 ES 标准并没有规定事件循环应该如何编写,因为这很明显取决于运行时,所以各个 Runtime 之间的事件循环存在较大的差别,比如说,之前浏览器和 Node 的事件循环机制就存在较大的差别。最新版的 Node 在事件循环上的表现,基本与浏览器类似了。

Node 的事件循环机制基于 libuv ,当你去阅读 Node 源码时,你会发现 Node 用的基本就是 libuv 默认的事件循环机制。可以说 Node 是 libuv 实践的一大例子。 Node 的事件循环与 libuv 没有本质的差别。

Node 的事件循环的顺序为:

  1. 更新循环事件,判断队列是否存活。
  2. 运行到期定时器。
  3. 回调回调函数。
  4. 运行 idle 句柄回调。
  5. 运行 prepare 句柄回调。
  6. Poll 进行 IO 循环。
  7. 运行 check 句柄回调。
  8. 回调关闭函数。

其中,最难被理解的是 idle 、 prepare 和 check 部分。这部分代码的主体结构是一样的,都是用宏生成的三个句柄。

idle 部分写入的回调将会在每次循环到达时调用,虽然它的名字叫 “idle” (空闲),但实际上是非常忙碌的,因此有人也提议叫“spin”。

prepare 和 check 部分其实相对类似,那么 idle 、 prepare 和 check 有什么用?

我们直接观察 Node 的实现大概就能知道一些。

idle 阶段放在回调函数之后, Node 在这一阶段主要放了诸如 GC 之类的事情,这部分是内部的,且每次都会被调用。之所以叫 idle 也是因为这里执行的回调是每次都执行但不知道放哪里好,就放在了一切回调完成之后。但这个也不是一个好的命名。

prepare 主要是处理在 poll 阶段之前的一些准备工作,有回调函数到这里了就会立刻执行完成,如果有 I/O 事件需要执行,就做一些准备工作。

check 主要是处理 I/O 完成之后,收集 poll 的结果,绑定到回调函数上的。

idlepreparecheck 三者都是观察者,这一点也可以从 libev 的文档里知道。

ev_idle 观察者已经确定你没有更好的事情要做。

所有 ev_prepare 观察者都在 ev_run 开始收集新事件之前调用,并且所有 ev_check 观察者在 ev_run 收集它们之后排队(未调用),但在它排队任何接收到的事件的回调之前。这意味着 ev_prepare 观察者是在事件循环休眠或轮询新事件之前调用的最后一个观察者,并且 ev_check 观察者将在事件循环迭代中的任何其他具有相同或较低优先级的观察者之前被调用。

两种观察者类型的回调可以像他们想要的那样开始和停止所有观察者,并且所有观察者都将被考虑在内(例如,ev_prepare 观察者可能启动空闲观察者以防止 ev_run 阻止)。

把 ev 换成 uv 也是一样的,毕竟 libev 是老父亲嘛!