关于node.js:nodejs事件和事件循环详解

5次阅读

共计 5108 个字符,预计需要花费 13 分钟才能阅读完成。

简介

上篇文章咱们简略的介绍了 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 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 之后才会真正的执行。

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

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

正文完
 0