深入node.js事件轮询

尽管 JavaScript 是单线程的,但“事件轮询”机制允许 Node.js 通过尽可能将操作系统内核执行非阻塞的I/O操作。

由于大多数现代内核都是多线程的,因此它们可以在后台处理执行多个操作。当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到轮询队列中以最终执行。


线程模型


JavaScript 从设计之初起就是单线程,它的目的就是解决一些脚本语言的问题,因为设计的能力有限,性能不需要重点考虑。

由于 JavaScript 是单线程的,所有任务都在一个线程上完成,一旦遇到大量任务或者遇到一个耗时的任务,网页就会出现"假死"。Event Loop就是为了解决这个问题而提出的。



EventLoop


简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。

EventLoop 是 Javascript 对异步的具体实现,在程序执行过程中,直接执行同步代码,异步代码/函数会先放在异步队列中挂起,待同步的任务执行完成之后,轮询执行异步队列中的任务。



Node.js中的EventLoop


Node.js 中的 EventLoop 是通过 libuv 这个库实现的。当 Node.js 启动时,它会初始化事件循环,处理在 Node.js 中运行的代码,其中可能包含了异步API调用,然后开始运行事件循环。node.js 处理的事件循环一共有六个阶段,如下面所示:

   ┌───────────────────────┐
┌─>│   timers(定时器阶段)    │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │ I/O callbacks(系统错误)│
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │     poll(轮询阶段)     │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

每个阶段的对应关系如下:

  • timers(定时器阶段):

    此阶段执行由 setTimeout 和 setInterval 调度的回调;

  • pending callbacks:

    此阶段执行系统错误处理,包括 socket、TCP、UDP等;

  • idle, prepare:

    此阶段仅仅内部使用;

  • poll:

    此阶段会检索新的I/O事件,执行对应I/O相关的回调(首先处理到期的定时器回调,然后处理 poll 队列中的回调,直到队列中的回调被清空,或者达到处理的上限;若队列不为空,且刚好有 setImmediate 回调,则终止当前 poll 阶段,前往 check 阶段,执行 setImmediate 回调;若没有 setImmediate 回调,node.js 回去查看有没有定时器任务到期,若有则前往 timer 阶段执行定时器任务,若没有则前往 check 阶段);

  • check:

    此阶段会执行 setImmediate 回调(注意 setImmediate 回调只能在此阶段执行);

  • close callbacks:

    一些关闭的回调,例如 ***.on('close',...);


以上六个阶段都是独立的,都存在有自己的消息队列,不同的回调一定会进入到不同的阶段(比如:setImmediate 一定会在 check 阶段,setTimeout 一定会在 timer 阶段)。

事件循环启动后,都是从上往下执行,每个阶段都会执行队列中回调,直到全部执行完,或者达到最大的处理数量。node.js 就会进入到下一个阶段去检查新的队列,直到执行完全部的阶段还有每个阶段中的队列。

注意:

node.js 在执行过程中,并不是严格按照这几个阶段的顺序执行的,有的阶段会被web事件触发。同时,在任意两个阶段切换之间,只要有 process.nextTick 回调没有执行,那么就优先执行它的回调,这也包括 Promise 这种 macrotask 回调。

最后更新时间: 2019-7-7 21:55:38