写点什么

nodejs 事件和事件循环详解

发布于: 2021 年 01 月 14 日

简介

上篇文章我们简单的介绍了 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 的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!


发布于: 2021 年 01 月 14 日阅读数: 28
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
nodejs事件和事件循环详解