1

Node中的事件循环

如果对前端浏览器的时间循环不太清楚,请看这篇文章。那么node中的事件循环是什么样子呢?其实官方文档有很清楚的解释,本文先从node执行一个单文件说起,再讲事件循环。

node的内部模块

任何高级语言的存在都有一定的执行环境,比如浏览器的代码是在浏览器引擎中,那么在node环境中也有一定的执行环境。我们先来看一下官网的依赖包有哪些?

  • V8
  • libuv
  • http-parser
  • c-cares
  • OpenSSL
  • zlib

上面就是nodejs中依赖的模块。那么这些模块之间是如何工作的呢?模块之间的工作关系如下图所示:


主要过程如下:

  • step1: 用户的代码通过v8引擎解释器,解析为两部分:"立即执行"和"异步执行"。

立即执行:可以理解为,需要v8引擎去处理的代码;
异步执行:并不是真正的异步,可以理解为,不需要v8引擎处理的和需要异步处理的。

  • step2: “异步执行”的部分,通过v8引擎和底层之间建立的绑定关系,去执行对应的操作
  • step3: 在“异步执行”部分,通过libuv内部的事件循环机制,无阻塞调用。libuv在执行的时候,主要通过handles和request实现对应的操作,handles和requests具备不同的数据结构。官网解释,handles是长期存在的对象,request是短期存在的对象,猜测来讲,requests和handles有不同的垃圾回收机制。

libuv的事件循环

一个线程有唯一的一个事件循环(event loop)。线程非安全。

这里需要理解两点:

  • 线程

这可能和我们理解的不太一样,Javascript代码是单线程的,但是libuv不是单线程的,他可以开启多个线程,libuv 提供了一个调度的线程池,线程池中的线程数目,默认是4个,最多1024个(为什么?因为每一个线程都会占用资源,而内存是有限的),关于线程池的可以看官方文档。

  • 线程安全

对数据的操作无非就是读和写,线程安全,简单来说,就是一个线程对这一份数据具有独占性,只有当该线程操作完成,其他线程才可以进行操作,当然线程安全的概念远不止这些,详细可以看维基百科,这里就简单理解一下就行了。

libuv中的事件循环

事件循环图,如下所示:

主要分为下面几步:

  • step1: 线程启动时,初始化一个时间:now,为了计算后面的timer的回调函数什么时候执行
  • step2: 判断事件循环是否存活,如果不存活,立即退出,否则进行下一步。判断是否存活的依据:索引是否存在。索引就是指否还有需要执行的事件,是否还有请求,关闭事件循环的请求等等。(用白话来讲,就是看还有没有没处理的事情)
  • step3: 执行所有的定时器(timers)在事件循环之前
  • step4: 执行待执行(pending)的回调,一般的IO轮询都会在轮询后,立即执行,但是有的也会延迟(defer)执行,延迟执行的,就会在这个阶段执行
  • step4: 执行空闲(idle)函数,每个阶段都会执行的,一般情况下是执行一些必要的操作,程序内置的
  • step5: 执行准备好的回调函数,具体内部使用的
  • step6: IO轮询执行,直到超时,在阻塞执行之前,会计算超时时间,也就是停止轮询的时间:

    • 如果队列为空、或者是即将关闭,或者有将要关闭的handles,timeout为0
    • 如果没有上面的情况,超时时间就取最近的timer时间,否则就是无穷大

(用白话来理解,就是看有没有要关闭的,有的话,就直接往下走,没有的话,看看有哪个事件比较急,到了点就去执行)

  • step7: 执行IO
  • step8: 检查接下来要执行哪些handle,保证正确执行
  • step9: 是否存在关闭的回调,如果有就执行,关闭循环,否则继续循环

通常情况下来讲,文件的I/O会调用线程池,但是网络请求的I/O总是用同一个线程。

Node中的事件循环

阻塞和非阻塞

node中所有的代码几乎都提供了同步(阻塞)和异步(非阻塞)的方式,你可以选择使用哪一种方式,但是不要混合使用

node中的事件循环,就是一个简版的libuv事件循环机制图

NodeJs中的定时器

NodeJs中的定时器主要有三种:

  • setTimeout
  • setInterval
  • setImmediate

三个定时器都有对应的取消函数:

  • clearTimeout
  • clearInterval
  • clearImmediate

setTimeout && setInterval

setTimeout和setInterval行为和在浏览器环境中的行为类似,但是setTimeout和setImmediate有一点不同。在libuv中可以看到,判断循环是否结束的时候,是需要判断是否还有待执行的函数,如果只剩下一个setTimeout或者setInterval函数,那么整个循环还会继续存在,node提供了一个函数,可以让循环暂时休眠

  • unref
  • ref

unref是可以让setTimeout暂时休眠,ref可以再次唤醒

setImmediate

setImmediate是指定在事件循环结束执行的。主要发生在poll阶段之后

如果poll队列没空,则一直执行,直到对列空位置

如果poll队列空了,有setImmediate事件,则会跳到check阶段

如果poll队列空了,没有setImmediate事件,就会查看哪一个timer事件快要到期了,转到timers阶段

依据上面的解释,就有了setTimeout和setImmediate执行先后顺序的问题:

setTimeout(() => {
  console.log('timeout');
})
setImmediate(() => {
  console.log('immediate);
});

先说答案:

可能会有两种情况:
timeout
immediate
或者
immediate
timeout

为什么?
主要是setTimeout在前或者后的问题,依赖于线程的执行速度。
主要是两个阶段:

  • 1、v8引擎执行环境扫描代码,启动事件循环,当走到setTimeout的时候,会将timeout丢进libuv事件队列中
  • 2、v8引擎继续执行,走到setImmediate

    • 此时,上面的libuv事件队列可能执行第一次,刚走到poll阶段,那么接下来就会打印immediate,
    • 也可能libuv事件队列,已经第二次循环,经过了poll阶段,然后判断timeout到时间了,去执行timeout了,这样就会先打印timeout然后再打印immediate

所以根本原因是在于事件循环执行了一次还是两次。

那我们接下来看看事件循环的逻辑

nextTick

Node添加了这样一个API,这个并不在事件循环的机制内,但是和时间循环机制相关。先来看一下定义:

nextTick的定义是在事件循环的下一个阶段之前执行对应的回调。

虽然nextTick是这样定义的,但是它并不是为了在事件循环的每个阶段去执行的。
主要有下面两种应用场景:

  • 作为下一个执行阶段的钩子,去清理不需要的资源,或者再次请求
  • 等运行环境准备好之后,再去执行回调

案例一:

let bar;

function someAsyncApiCall(callback) {
  callback()
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

// 输出
undefined
1

输出undefine的情况是,因为执行函数的时候,bar并没有被赋值,而process.nextTick则能保证整个执行环境都准备好了再去执行

案例二:

const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });

当v8引擎执行完代码后,listen的回调会直接命中poll阶段,那么server的connect事件就不会执行

案例三:

想要在构造函数中,去发送对应的事件,因为此时v8引擎还没有扫描到,而构造函数的代码会立即执行,就需要nextTick

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  // 这样操作无效
  this.emit('event');
  // 应该这样
  // process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

总结

上面三个案例,重点在于v8引擎是单线程立即执行,而libuv则是异步执行,想要在异步循环之前执行一些操作就需要process.nextTick

参考文档

Node官网解释
libuv的设计
关于libuv的概念详细解释
libuv线程池实现
并发


joytime
44 声望2 粉丝