Node中的事件循环

Node中的事件循环

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

node的内部模块

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

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

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


主要过程以下:git

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

当即执行:能够理解为,须要v8引擎去处理的代码;
异步执行:并非真正的异步,能够理解为,不须要v8引擎处理的和须要异步处理的。github

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

libuv的事件循环

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

这里须要理解两点:web

  • 线程

这可能和咱们理解的不太同样,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在前或者后的问题,依赖于线程的执行速度。
主要是两个阶段:

  • 一、v8引擎执行环境扫描代码,启动事件循环,当走到setTimeout的时候,会将timeout丢进libuv事件队列中
  • 二、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线程池实现
并发

相关文章
相关标签/搜索