Node中的事件循环如果你对前端浏览器的时间循环还不是很清楚,请看这篇文章。那么node中的事件循环是什么样子的呢?其实官方文档有很明确的解释。本文从node执行单个文件开始,然后讲事件循环。node内部模块中任何高级语言的存在,都有一定的执行环境。比如浏览器的代码在浏览器引擎中,那么在node环境中也有一定的执行环境。我们先来看看官网有哪些依赖包?上面的v8libuvhttp-parserc-caresOpenSSLzlib是nodejs依赖的模块。那么这些模块如何相互协作呢?模块之间的工作关系如下图所示:主要流程如下:step1:用户的代码被v8引擎解释器解析为两部分:“立即执行”和“异步执行”。立即执行:可以理解为需要v8引擎处理的代码;异步执行:并不是真正的异步,可以理解为不需要v8引擎处理,需要异步处理的代码。step2:在“异步执行”部分,通过v8引擎与底层建立的绑定关系,进行相应的操作。step3:在“异步执行”部分,通过libuv内部的事件循环机制进行非阻塞调用。libuv在执行时主要通过handle和requests来实现相应的操作,handle和requests有不同的数据结构。官网解释handle是long-livedobject,requests是short-livedobject。猜测,请求和句柄有不同的垃圾收集机制。libuv的事件循环一个线程只有一个事件循环(eventloop)。线程不安全。这里有两点需要理解:thread这个可能和我们理解的不太一样。Javascript代码是单线程的,但libuv不是单线程的。它可以打开多个线程。libuv提供了一个线程池用于调度。线程池中的线程数,默认是4个,最大是1024个(为什么?因为每个线程都会占用资源,内存是有限的),关于线程池可以看官方文档。线程安全对数据的操作无非就是读写。线程安全,简单来说就是一个线程可以独占访问这块数据。只有当线程操作完成后,其他线程才能操作。当然,线程安全的概念远不止这些,具体可以看维基百科,这里简单了解一下。libuv中的eventloop事件循环图如下:主要分为以下几个步骤:step1:线程启动时,初始化一个时间:now,以此来计算定时器的回调函数什么时候执行step2:判断事件循环是否存活,若不存活则立即退出,否则进行下一步。判断是否存活的依据:索引是否存在。索引是指是否有事件需要执行,是否还有请求,关闭事件循环的请求等等。(大白话就是看是否还有未处理的东西)step3:执行事件循环之前的所有定时器(timers)step4:执行pendingcallbacks,一般IOpolling会在polling之后,马上执行,但是有些还会延迟(defer)执行。如果延迟执行,step4会在这个阶段执行:执行空闲(idle)函数,每个阶段都会执行。一般会执行一些必要的功能。运行,程序内置step5:执行准备好的回调函数,具体内部step6:IO轮询执行到超时,阻塞执行前会计算超时时间,即停止轮询的时间:如果队列为空,或者即将关闭,或者有即将关闭的句柄,超时为0。如果没有上述情况,超时时间取最近的定时器时间,否则为infinite如果没有就直接往下走,如果没有就看哪个事件比较紧急,到了就执行)step7:执行IOstep8:检查接下来要执行哪些句柄,保证正确执行callback,如果有则执行,关闭循环,否则继续循环。一般来说,文件的I/O会调用线程池,但是网络请求的I/O总是使用同一个线程。Node中的事件循环阻塞和非阻塞Node中几乎所有的代码都提供了同步(阻塞)和异步(非阻塞)方法。您可以选择使用哪种方法,但不要混合使用。node中的事件循环是libuv事件循环机制的简化版。NodeJs中的定时器NodeJs中的定时器主要分为三种:setTimeoutsetIntervalsetImmediate这三种定时器都有相应的取消函数:clearTimeoutclearIntervalclearImmediatesetTimeout&&setIntervalsetTimeout和setInterval的行为和在浏览器环境下的行为类似,只是setTimeout和setImmediate有点不同。在libuv中可以看到,在判断循环是否结束的时候,需要判断是否有函数要执行。如果只剩下一个setTimeout或setInterval函数,那么整个循环将继续存在。Node提供了一个函数,可以让循环暂时休眠。unrefrefunref可以让setTimeout暂时休眠,ref可以再次唤醒setImmediatesetImmediate指定在事件循环结束时执行。它主要发生在轮询阶段之后。如果轮询队列不为空,则继续执行,直到相应的列为空。如果轮询队列为空,且有setImmediate事件,则跳转到检查阶段。如果poll队列为空,没有setImmediate事件,会检查哪个timer事件即将过期,进入timers阶段根据上面的解释,setTimeout和setImmediate的执行顺序有问题:setTimeout(()=>{console.log('timeout');})setImmediate(()=>{console.log('immediate');});先说答案:可能有两种情况:timeoutimmediate或者immediatetimeoutwhy?主要问题是setTimeout是在之前还是之后,取决于线程的执行速度。主要有两个阶段:1.v8引擎执行环境扫描代码,启动事件循环。当它到达setTimeout时,它会将超时抛入libuv事件队列中。2.v8引擎继续执行。当到达setImmediate时,上面的libuv事件队列可能是第一次执行,正好到了poll阶段,那么接下来会打印immediate,也可能是libuv事件队列已经进行了第二次循环,poll之后stage,然后判断超时,执行timeout,这样会先打印timeout,然后immediate,所以根本原因是eventloop执行了一次还是执行了两次。然后再看事件循环的逻辑nextTickNode增加了这样一个API,它不在事件循环机制中,而是和时间循环机制有关。我们先看一下定义:nextTick的定义是在事件循环的下一阶段之前执行相应的回调。虽然nextTick是这样定义的,但它并不打算在事件循环的每个阶段都执行。主要有以下两种应用场景:作为下一个执行阶段的hook,清理不需要的资源,或者运行环境准备好后再次请求,然后执行回调Case1:letbar;functionsomeAsyncApiCall(callback){callback()process.nextTick(callback);}someAsyncApiCall(()=>{console.log('bar',bar);//1});bar=1;//输出undefined1输出undefine是因为执行函数,bar还没有赋值,process.nextTick可以保证整个执行环境在执行Case2之前准备好:constserver=net.createServer();server.on('connection',(conn)=>{});server.listen(8080);server.on('监听',()=>{});v8引擎执行完代码后,listen回调会直接打到poll阶段,那么server的connect事件就不会执行Case3:如果要在constructor中发送相应的事件,因为v8引擎还没有还没有扫描,构造函数的代码会立即执行,你需要nextTick//这是无效的this.emit('event');//它应该是这样的//process.nextTick(()=>{this.emit('event');});}util.inherits(MyEmitter,EventEmitter);constmyEmitter=newMyEmitter();myEmitter.on('事件',()=>{console.log('事件发生!');});总结以上三种情况,重点是v8引擎是单线程的,立即执行,而libuv是异步执行的。如果想在异步循环之前执行一些操作,需要process.nextTick参考文档Node官网讲解libuv的设计详解libuv概念libuv线程池并发的实现
