查看原文
其他

关于NodeJS工作原理的五个误解

脚本之家 2021-06-30

The following article is from code秘密花园 Author ConardLi

  脚本之家

你与百万开发者在一起

本文经授权转自公众号 code秘密花园(ID:code_mmhy)

如若转载请联系原公众号

英文原文:https://blog.insiderattack.net/five-misconceptions-on-how-nodejs-works-edfb56f7b3a6授权译者:ConardLi

NodeJS诞生于2009年,由于它使用了JavaScript,在这些年里获得了非常广泛的流行。它是一个用于编写服务器端应用程序的JavaScript运行时,但是"它就是JavaScript"这句话并不是100%正确的。

JavaScript是单线程的,它不是被设计用来实现要求可伸缩性的服务器端上运行的。借助Google Chrome的高性能V8 JavaScript引擎,libuv的超酷异步I/O实现以及其他一些刺激性的补充,NodeJS能够将客户端JavaScript引入服务器端,从而能够编写超快速的、能够处理成千上万的套接字连接的Web JavaScript服务器。

如上图所示,NodeJS是一个由大量有趣的基础模块构建的大型平台。但是,由于对NodeJS的这些内部组件的工作方式缺乏了解,因此许多NodeJS开发人员对NodeJS的行为做出了错误的理解,并开发了导致严重性能问题以及难以跟踪的错误的应用程序。在本文中,我将描述在许多NodeJS开发人员中很常见的五个错误理解。

误解1 — EventEmitter 和事件循环相关

编写NodeJS应用程序时会大量使用NodeJS EventEmitter,但是人们误认为EventEmitterNodeJS Event Loop有关,这是不正确的。

NodeJS事件循环是NodeJS的核心,它为NodeJS提供了异步的,非阻塞的I/O机制。它以特定顺序处理来自不同类型的异步事件的完成事件。

相反,NodeJS Event Emitter是一个核心的NodeJS API,它允许你将监听器函数附加到一个特定的事件,这个事件一旦触发就会被调用。这种行为看起来像是异步的,因为事件处理程序的调用时间通常比它最初作为事件处理程序注册的时间晚。

EventEmitter实例跟踪与EventEmitter实例本身内的事件相关联的所有事件和其实例本身。它不会在事件循环队列中调度任何事件。存储此信息的数据结构只是一个普通的老式JavaScript对象,其中对象属性是事件名称,属性的值是一个侦听器函数或侦听器函数数组。

当在EventEmitter实例上调用emit函数时,emitter将按顺序依次同步调所有注册到示例上的回调函数。

看以下代码片段:

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

myEmitter.on('myevent', () => console.log('handler1: myevent was fired!'));
myEmitter.on('myevent', () => console.log('handler2: myevent was fired!'));
myEmitter.on('myevent', () => console.log('handler3: myevent was fired!'));

myEmitter.emit('myevent');
console.log('I am the last log line');

以上代码段的输出为:

handler1: myevent was fired!
handler2: myevent was fired!
handler3: myevent was fired!
I am the last log line

由于event emitter同步执行所有事件处理函数,因此I am the last log line在调用所有监听函数完成之后才会打印。

误解2 - 所有接受回调的函数都是异步的

函数是同步的还是异步的取决于函数在执行期间是否创建异步资源。根据这个定义,如果给你一个函数,你可以确定给定的函数是异步的:

  • 调用本地JavaScript/ 异步的NodeJS功能(例如,setTimeout,setInterval,setImmediate,process.nextTick,等等)
  • 执行异步的NodeJS API(例如,异步函数child_process,fs,net等等)使用PromiseAPI(包括使用async-await
  • C++插件调用一个函数,该函数被编写为异步函数(例如bcrypt

接受回调函数作为参数不会使函数异步。但是,通常异步函数的确接受回调作为最后一个参数(除非包装返回一个Promise)。接受回调并将结果传递给回调的这种模式称为Continuation Passing Style。你仍然可以使用Continuation Passing Style编写同步功能。

const sum = (a, b, callback) => {
callback(a + b);
};

sum(1,2, (result) => {
console.log(result);
});

同步函数和异步函数在执行期间在如何使用堆栈方面有很大的不同。同步函数在执行的整个过程中都会占用堆栈,方法是禁止其他任何人占用堆栈直到return 为止。相反,异步函数调度一些异步任务并立即返回,因此将自身从堆栈中删除。一旦预定的异步任务完成,将调用提供的任何回调,并且该回调函数将再次占据该堆栈。此时,启动异步任务的函数将不再可用,因为它已经返回。

考虑到以上定义,请尝试确定以下函数是异步还是同步。


function writeToMyFile(data, callback) {
if (!data) {
callback(new Error('No data provided'));
} else {
fs.writeFile('myfile.txt', data, callback);
}
}

实际上,上述函数可以是同步的,也可以是异步的,具体取决于传递给的值data

如果data为 false,callback则将立即调用,并出现错误。在此执行路径中,该功能是100%同步的,因为它不执行任何异步任务。

如果data是 true ,它会将data写入myfile.txt,将调用回调完成的文件I/O操作之后。由于异步文件I/O操作,此执行路径是100%异步的。

强烈建议不要以这种不一致的方式(在此功能同时执行同步和异步操作)编写函数,因为这会使应用程序的行为无法预测。幸运的是,这些不一致可以很容易地修复如下:

function writeToMyFile(data, callback) {
if (!data) {
process.nextTick(() => callback(new Error('No data provided')));
} else {
fs.writeFile('myfile.txt', data, callback);
}
}

process.nextTick可以用来延迟callback函数的调用,从而使执行路径异步。

或者,你可以使用 setImmediate 代替 process.nextTick ,这或多或少会产生相同的结果。但是,process.nextTick相对而言,回调具有更高的优先级,从而使其比 setImmediate 更快。

误解3 - 所有占用大量CPU的功能都在阻止事件循环

众所周知,CPU密集型操作会阻塞Node.js事件循环。尽管这句话在一定程度上是正确的,但并不是100%正确,因为有些CPU密集型函数不会阻塞事件循环。

一般来说,加密操作和压缩操作是受CPU高度限制的。由于这个原因,某些加密函数和zlib函数的异步版本以在libuv线程池上执行计算的方式编写,这样它们就不会阻塞事件循环。其中一些功能是:

  • crypto.pbkdf2()
  • crypto.randomFill()
  • crypto.randomBytes()
  • 所有zlib异步功能

但是,在撰写本文时,还无法使用纯JavaScriptlibuv线程池上运行CPU密集型操作。但是,你可以编写自己的C++插件,使你能够安排libuv线程池上的工作。有某些第三方库(例如bcrypt),它们执行CPU密集型操作并使用C++插件来实现针对CPU绑定操作的异步API。

误解4 - 所有异步操作都在线程池上执行

现代操作系统具有内置的内核支持,可使用事件通知(例如,Linux中的epollmacOS中的kqueueWindows中的IOCP等)以有效的方式促进网络I/O操作的本机异步。因此,不会在libuv线程池上执行网络I/O

但是,当涉及到文件I/O时,跨操作系统以及同一操作系统中的某些情况存在许多不一致之处。这使得为文件I/O实现通用的独立于平台的API极为困难。因此,在libuv线程池上执行文件系统操作以公开一致的异步API

dns.lookup() dns模块中的函数是另一个利用libuv线程池的API。原因是,使用dns.lookup()功能将域名解析为IP地址是与平台有关的操作,并且此操作不是100%的网络I/O

误解5 - 不应使用NodeJS编写CPU密集型应用程序

这并不是真正的误解,而是关于NodeJS的一个众所周知的事实,现在由于在Node v10.5.0中引入Worker Threads而被淘汰了。尽管它是作为实验性功能引入的,但worker_threadsNode v12 LTS起,该模块现已稳定,因此适合在具有CPU密集型操作的生产应用程序中使用。

每个Node.js工作线程将拥有其自己的v8运行时的副本,事件循环和libuv线程池。因此,执行阻塞CPU密集型操作的一个工作线程不会影响其他工作线程的事件循环,从而使它们可用于任何传入的工作。

但是,在撰写本文时,IDE对Worker Threads的支持还不是最大。某些IDE不支持将调试器附加到在主线程以外的其他线程中运行的代码。但是,随着许多开发人员已经开始采用辅助线程进行CPU绑定的操作(例如视频编码等),开发支持将随着时间的推移而成熟。

- END -



更多精彩


在公众号后台对话框输入以下关键词

查看更多优质内容!


女朋友 | 大数据 | 运维 | 书单 | 算法

大数据 | JavaScript | Python | 黑客

AI | 人工智能 | 5G | 区块链

机器学习 | 数学 | 送书

●  Vue组件入门篇 —— 表单组件

●  脚本之家粉丝福利,请查看!

●  人人都欠微软一个正版?

● 面试官问:Node 与底层之间如何执行异步 I/O 调用?

 Node.js 使用 express-jwt 解析 JWT

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存