上篇文章咱们简单的介绍了nodejs中的事件event和事件循环event loop。本文本文将会更进一步,继续讲解nodejs中的event,并探讨一下setTimeout,setImmediate和process.nextTick的区别。node
虽然nodejs是单线程的,可是nodejs能够将操做委托给系统内核,系统内核在后台处理这些任务,当任务完成以后,通知nodejs,从而触发nodejs中的callback方法。linux
这些callback会被加入轮循队列中,最终被执行。web
经过这样的event loop设计,nodejs最终能够实现非阻塞的IO。windows
nodejs中的event loop被分红了一个个的phase,下图列出了各个phase的执行顺序:异步
每一个phase都会维护一个callback queue,这是一个FIFO的队列。socket
当进入一个phase以后,首先会去执行该phase的任务,而后去执行属于该phase的callback任务。函数
当这个callback队列中的任务所有都被执行完毕或达到了最大的callback执行次数以后,就会进入下一个phase。oop
注意, windows和linux的具体实现有稍许不一样,这里咱们只关注最重要的几个phase。
问题:phase的执行过程当中,为何要限制最大的callback执行次数呢?ui
回答:在极端状况下,某个phase可能会须要执行大量的callback,若是执行这些callback花费了太多的时间,那么将会阻塞nodejs的运行,因此咱们设置callback执行的次数限制,以免nodejs的长时间block。this
上面的图中,咱们列出了6个phase,接下来咱们将会一一的进行解释。
timers的中文意思是定时器,也就是说在给定的时间或者时间间隔去执行某个callback函数。
一般的timers函数有这样两种:setTimeout和setInterval。
通常来讲这些callback函数会在到期以后尽量的执行,可是会受到其余callback执行的影响。 咱们来看一个例子:
const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } });
上面的例子中,咱们调用了someAsyncOperation,这个函数首先回去执行readFile方法,假设这个方法耗时95ms。接着执行readFile的callback函数,这个callback会执行10ms。最后才回去执行setTimeout中的callback。
因此上面的例子中,虽然setTimeout指定要在100ms以后运行,可是实际上还要等待95 + 10 = 105 ms以后才会真正的执行。
这个phase将会执行一些系统的callback操做,好比在作TCP链接的时候,TCP socket接收到了ECONNREFUSED信号,在某些liunx操做系统中将会上报这个错误,那么这个系统的callback将会放到pending callbacks中运行。
或者是须要在下一个event loop中执行的I/O callback操做。
idle, prepare是内部使用的phase,这里就不过多介绍。
poll将会检测新的I/O事件,并执行与I / O相关的回调,注意这里的回调指的是除了关闭callback,timers,和setImmediate以外的几乎全部的callback事件。
poll主要处理两件事情:轮询I/O,而且计算block的时间,而后处理poll queue中的事件。
若是poll queue非空的话,event loop将会遍历queue中的callback,而后一个一个的同步执行,知道queue消费完毕,或者达到了callback数量的限制。
由于queue中的callback是一个一个同步执行的,因此可能会出现阻塞的状况。
若是poll queue空了,若是代码中调用了setImmediate,那么将会立马跳到下一个check phase,而后执行setImmediate中的callback。 若是没有调用setImmediate,那么会继续等待新来的callback被加入到queue中,并执行。
主要来执行setImmediate的callback。
setImmediate能够看作是一个运行在单独phase中的独特的timer,底层使用的libuv API来规划callbacks。
通常来讲,若是在poll phase中有callback是以setImmediate的方式调用的话,会在poll queue为空的状况下,立马结束poll phase,进入check phase来执行对应的callback方法。
最后一个phase是处理close事件中的callbacks。 好比一个socket忽然被关闭,那么将会触发一个close事件,并调用相关的callback。
setTimeout和setImmediate有什么不一样呢?
从上图的phase阶段能够看出,setTimeout中的callback是在timer phase中执行的,而setImmediate是在check阶段执行的。
从语义上讲,setTimeout指的是,在给定的时间以后运行某个callback。而setImmediate是在执行完当前loop中的 I/O操做以后,立马执行。
那么这两个方法的执行顺序上有什么区别呢?
下面咱们举两个例子,第一个例子中两个方法都是在主模块中运行:
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
这样运行两个方法的执行顺序是不肯定,由于可能受到其余执行程序的影响。
第二个例子是在I/O模块中运行这两个方法:
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
你会发现,在I/O模块中,setImmediate必定会在setTimeout以前执行。
setTimeout和setImmediate二者都有一个返回值,咱们能够经过这个返回值,来对timer进行clear操做:
const timeoutObj = setTimeout(() => { console.log('timeout beyond time'); }, 1500); const immediateObj = setImmediate(() => { console.log('immediately executing immediate'); }); const intervalObj = setInterval(() => { console.log('interviewing the interval'); }, 500); clearTimeout(timeoutObj); clearImmediate(immediateObj); clearInterval(intervalObj);
clear操做也能够clear intervalObj。
setTimeout和setInterval返回的对象都是Timeout对象。
若是这个timeout对象是最后要执行的timeout对象,那么可使用unref方法来取消其执行,取消执行完毕,可使用ref来恢复它的执行。
const timerObj = setTimeout(() => { console.log('will i run?'); }); timerObj.unref(); setImmediate(() => { timerObj.ref(); });
注意,若是有多个timeout对象,只有最后一个timeout对象的unref方法才会生效。
process.nextTick也是一种异步API,可是它和timer是不一样的。
若是咱们在一个phase中调用process.nextTick,那么nextTick中的callback会在这个phase完成,进入event loop的下一个phase以前完成。
这样作就会有一个问题,若是咱们在process.nextTick中进行递归调用的话,这个phase将会被阻塞,影响event loop的正常执行。
那么,为何咱们还会有process.nextTick呢?
考虑下面的一个例子:
let bar; function someAsyncApiCall(callback) { callback(); } someAsyncApiCall(() => { console.log('bar', bar); // undefined }); bar = 1;
上面的例子中,咱们定义了一个someAsyncApiCall方法,里面执行了传入的callback函数。
这个callback函数想要输出bar的值,可是bar的值是在someAsyncApiCall方法以后被赋值的。
这个例子最终会致使输出的bar值是undefined。
咱们的本意是想让用户程序执行完毕以后,再调用callback,那么咱们可使用process.nextTick来对上面的例子进行改写:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
咱们再看一个实际中使用的例子:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
上面的例子是最简单的nodejs建立web服务。
上面的例子有什么问题呢?listen(8000) 方法将会立马绑定8000端口。可是这个时候,server的listening事件绑定代码尚未执行。
这里实际上就用到了process.nextTick技术,从而无论咱们在什么地方绑定listening事件,均可以监听到listen事件。
process.nextTick 是立马在当前phase执行callback,而setImmediate是在check阶段执行callback。
因此process.nextTick要比setImmediate的执行顺序优先。
实际上,process.nextTick和setImmediate的语义应该进行互换。由于process.nextTick表示的才是immediate,而setImmediate表示的是next tick。
本文做者:flydean程序那些事本文连接:http://www.flydean.com/nodejs-event-more/
本文来源:flydean的博客
欢迎关注个人公众号:「程序那些事」最通俗的解读,最深入的干货,最简洁的教程,众多你不知道的小技巧等你来发现!