NodeJS 中的事件循环,读了这篇就全懂了 (2)

在轮询阶段的执行过程中,一旦轮询队列为空,事件循环将检查是否有到期的定制器。如果一个或多个定时器已准备就绪,则事件循环将绕回定时器阶段以执行这些定时器的回调。

这里要特别对 setImmediate() 进行一些说明。

在 libuv 的事件循环中,允许开发人员在轮询阶段之前做些准备操作,然后在轮询阶段之后立即对这些操作进行检查。NodeJS 中 setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊定时器。它使用一个 libuv API 来安排回调在轮询阶段完成后执行。

setImmediate、setTimeout 和 process.nextTick

setImmediate() 被设计为一旦在当前轮询阶段完成,就执行代码。

setTimeout() 是在最小阈值(ms 单位)过后执行代码。

process.nextTick() 严格意义上讲并不属于事件循环的一部分。它不管事件循环的当前阶段如何,它都将在当前操作完成后处理 nextTickQueue 中排队的代码。

setImmediate() 和 setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

我们看下面这段代码:

setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });

这两个函数调用都在主模块中被调用,则他们的回调执行顺序是不定的,受进程的性能影响很大(进程会受到系统中运行其他应用程序影响)。

但是一旦将这两个函数放到 I/O 轮询调用内,那么 setImmediate() 一定会在 setTimeout() 之前被执行,不管有多个定制器已经到期。比如下面这段代码,总是会先输出 "immediate"。

const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });

process.nextTick() 和 setImmediate() 严格意义上来说,应该将名称互换。因为 process.nextTick() 比 setImmediate() 触发得更快。

任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。之所以这么设计,是考虑到这些使用场景:

允许开发者处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。

有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

比如下面这段代码:

const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});

只有传递端口时,端口才会立即被绑定,然后立即调用 'listening' 回调。问题是 .on('listening') 的回调在那个时间点尚未被设置。

为了绕过这个问题,'listening' 事件被排在 nextTick() 中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。

Promise

这里在补充说明一下 NodeJS 中 Promise 是如何处理的。我们之前说过,在浏览器的事件循环里,会有一个微任务的队列来防止所有的微任务,并且在每个操作之后,都尝试清空微任务队列。

在 NodeJS 中,做法类似,NodeJS 的事件循环中也有一个微任务队列,工作机制与 process.nextTick() 类似,在每个操作之后,事件循环都会尝试清空微任务队列。

总结

我们结合 libuv 的事件循环,详细说明了 NodeJS 事件循环的每一阶段的具体职能。同时,我们还分析了常用的几个异步代码函数的原理。

我们用一张图归纳如下:

事件循环

常见面试知识点、技术解决方案、教程,都可以扫码关注公众号“众里千寻”获取,或者来这里 https://everfind.github.io 。

众里千寻

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzgddw.html