简介

上篇文章咱们简略的介绍了nodejs中的事件event和事件循环event loop。本文本文将会更进一步,持续解说nodejs中的event,并探讨一下setTimeout,setImmediate和process.nextTick的区别。

nodejs中的事件循环

尽管nodejs是单线程的,然而nodejs能够将操作委托给零碎内核,零碎内核在后盾解决这些工作,当工作实现之后,告诉nodejs,从而触发nodejs中的callback办法。

这些callback会被退出轮循队列中,最终被执行。

通过这样的event loop设计,nodejs最终能够实现非阻塞的IO。

nodejs中的event loop被分成了一个个的phase,下图列出了各个phase的执行程序:

每个phase都会保护一个callback queue,这是一个FIFO的队列。

当进入一个phase之后,首先会去执行该phase的工作,而后去执行属于该phase的callback工作。

当这个callback队列中的工作全副都被执行结束或达到了最大的callback执行次数之后,就会进入下一个phase。

留神, windows和linux的具体实现有稍许不同,这里咱们只关注最重要的几个phase。

问题:phase的执行过程中,为什么要限度最大的callback执行次数呢?

答复:在极其状况下,某个phase可能会须要执行大量的callback,如果执行这些callback破费了太多的工夫,那么将会阻塞nodejs的运行,所以咱们设置callback执行的次数限度,以防止nodejs的长时间block。

phase详解

下面的图中,咱们列出了6个phase,接下来咱们将会一一的进行解释。

timers

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 completesomeAsyncOperation(() => {  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之后才会真正的执行。

pending callbacks

这个phase将会执行一些零碎的callback操作,比方在做TCP连贯的时候,TCP socket接管到了ECONNREFUSED信号,在某些liunx操作系统中将会上报这个谬误,那么这个零碎的callback将会放到pending callbacks中运行。

或者是须要在下一个event loop中执行的I/O callback操作。

idle, prepare

idle, prepare是外部应用的phase,这里就不过多介绍。

poll轮询

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中,并执行。

check

次要来执行setImmediate的callback。

setImmediate能够看做是一个运行在独自phase中的独特的timer,底层应用的libuv API来布局callbacks。

一般来说,如果在poll phase中有callback是以setImmediate的形式调用的话,会在poll queue为空的状况下,立马完结poll phase,进入check phase来执行对应的callback办法。

close callbacks

最初一个phase是解决close事件中的callbacks。 比方一个socket忽然被敞开,那么将会触发一个close事件,并调用相干的callback。

setTimeout 和 setImmediate的区别

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。

unref 和 ref

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

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 和 setImmediate 的区别

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的博客

欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!