事件循环

参考:

简介

Node.js 是单进程单线程应用程序,那么它是如何实现异步调用的呢?

它维护了六个 FIFO 队列,分别表示不同的功能的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

然后它的单线程只执行一个 while 循环,一直跑这六个的头部任务,直到队列为空或者执行时间到达上限。

每个阶段执行的功能如下:

  1. timer:这个阶段执行通过 setTimeout()setInterval() 设置的回调函数;

  2. I/O callback:执行延迟到下一个循环迭代的 I/O 回调;

  3. idle,prepare:系统调用,也就是 liuv 调用;

  4. poll:轮询阶段,检测新的 I/O 事件,执行与 I/O 相关的回调(几乎所有的回调都是关闭回调,定时器调度的回调,以及 setImmaditate()),node会在此阶段适当的阻塞

  5. check:此阶段调用 setImmadiate() 设置的回调

  6. close callbacks:一些关闭回调,比如说 socket.on('close',...)

在每个队列阶段处理完之后,并不是直接进入下一阶段,而是会处理一些中间队列:

  1. 有 4 个主要类型的队列,被原生的 libuv 事件循环处理。
    • 过期计时器和间隔队列(Expired timers and intervals Queue):实际是最小堆存储;
    • IO 事件队列(IO Events Queue):完成的 I/O 事件;
    • 立即执行的队列(Immediate Queue):使用 setImmediate 函数添加的回调;
    • 关闭操作队列(Close Handlers Queue):任何一个 close 事件处理器。
  2. 除了四个主要的队列,这里另外有两个被 Node 上层处理的队列:
    • 下一个运转队列(Next Ticks Queue):使用 process.nextTick() 函数添加的回调
    • 其他的微队列(other Microtasks Queue):包含其他的微队列如成功的 Promise 回调

事件循环给了 nodejs 这些特性:

  • Node.js 几乎每一个 API 都是支持回调函数的。

  • 这些接口可以处理大量的并发,所以性能非常高。

  • Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。

事件驱动程序

Node.js 使用事件驱动模型,当 web server 接收到请求,就把它关闭然后进行处理,然后去服务下一个 web 请求。当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。

这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式 IO 或者事件驱动 IO)

在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

https://www.runoob.com/wp-content/uploads/2015/09/event_loop.jpg

整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题 (Subject),而所有注册到这个事件上的处理函数相当于观察者 (Observer)。

Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下实例:

1
2
3
4
// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

以下程序绑定事件处理程序:

1
2
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);

我们可以通过程序触发事件:

1
2
// 触发事件
eventEmitter.emit('eventName');