NodeJS开发人员常见五个错误理解
Nodejs诞生于2009年,由于它使用了JavaScript,在这些年里获得了非常广泛的流行。它是一个用于编写服务器端应用程序的JavaScript运行时,但是"它就是JavaScript"这句话并不是100%正确的。
JavaScript是单线程的,它不是被设计用来实现要求可伸缩性的服务器端上运行的。借助GoogleChrome的高性能V8JavaScript引擎,libuv的超酷异步I/O实现以及其他一些刺激性的补充,Nodejs能够将客户端JavaScript引入服务器端,从而能够编写超快速的、能够处理成千上万的套接字连接的WebJavaScript服务器。
NodeJS是一个由大量有趣的基础模块构建的大型平台。但是,由于对NodeJS的这些内部组件的工作方式缺乏了解,因此许多NodeJS开发人员对NodeJS的行为做出了错误的理解,并开发了导致严重性能问题以及难以跟踪的错误的应用程序。在本文中,我将描述在许多NodeJS开发人员中很常见的五个错误理解。
误解1—EventEmitter和事件循环相关
编写NodeJS应用程序时会大量使用NodeJSEventEmitter,但是人们误认为EventEmitter与NodeJSEventLoop有关,这是不正确的。
NodeJS事件循环是NodeJS的核心,它为NodeJS提供了异步的,非阻塞的I/O机制。它以特定顺序处理来自不同类型的异步事件的完成事件。
相反,NodeJSEventEmitter是一个核心的NodeJSAPI,它允许你将监听器函数附加到一个特定的事件,这个事件一旦触发就会被调用。这种行为看起来像是异步的,因为事件处理程序的调用时间通常比它最初作为事件处理程序注册的时间晚。
EventEmitter实例跟踪与EventEmitter实例本身内的事件相关联的所有事件和其实例本身。它不会在事件循环队列中调度任何事件。存储此信息的数据结构只是一个普通的老式JavaScript对象,其中对象属性是事件名称,属性的值是一个侦听器函数或侦听器函数数组。
当在EventEmitter实例上调用emit函数时,emitter将按顺序依次同步调所有注册到示例上的回调函数。
看以下代码片段:
constEventEmitter=require('events');
constmyEmitter=newEventEmitter();
myEmitter.on('myevent',()=>console.log('handler1:myeventwasfired!'));
myEmitter.on('myevent',()=>console.log('handler2:myeventwasfired!'));
myEmitter.on('myevent',()=>console.log('handler3:myeventwasfired!'));myEmitter.emit('myevent');
console.log('Iamthelastlogline');
以上代码段的输出为:
handler1:myeventwasfired!
handler2:myeventwasfired!
handler3:myeventwasfired!
Iamthelastlogline
由于eventemitter同步执行所有事件处理函数,因此Iamthelastlogline在调用所有监听函数完成之后才会打印。
误解2-所有接受回调的函数都是异步的
函数是同步的还是异步的取决于函数在执行期间是否创建异步资源。根据这个定义,如果给你一个函数,你可以确定给定的函数是异步的:
JavaScript
NodeJS
setTimeout,setInterval,setImmediate,process.nextTick
NodeJSAPI
child_process,fs,net
PromiseAPI
async-await
从C++插件调用一个函数,该函数被编写为异步函数(例如bcrypt)
接受回调函数作为参数不会使函数异步。但是,通常异步函数的确接受回调作为最后一个参数(除非包装返回一个Promise)。接受回调并将结果传递给回调的这种模式称为ContinuationPassingStyle。你仍然可以使用ContinuationPassingStyle编写同步功能。
constsum=(a,b,callback)=>{ callback(a+b); }; sum(1,2,(result)=>{ console.log(result); });
同步函数和异步函数在执行期间在如何使用堆栈方面有很大的不同。同步函数在执行的整个过程中都会占用堆栈,方法是禁止其他任何人占用堆栈直到return为止。相反,异步函数调度一些异步任务并立即返回,因此将自身从堆栈中删除。一旦预定的异步任务完成,将调用提供的任何回调,并且该回调函数将再次占据该堆栈。此时,启动异步任务的函数将不再可用,因为它已经返回。
考虑到以上定义,请尝试确定以下函数是异步还是同步。
functionwriteToMyFile(data,callback){ if(!data){ callback(newError('Nodataprovided')); }else{ fs.writeFile('myfile.txt',data,callback); } }
实际上,上述函数可以是同步的,也可以是异步的,具体取决于传递给的值data。
如果data为false,callback则将立即调用,并出现错误。在此执行路径中,该功能是100%同步的,因为它不执行任何异步任务。
如果data是true,它会将data写入myfile.txt,将调用回调完成的文件I/O操作之后。由于异步文件I/O操作,此执行路径是100%异步的。
强烈建议不要以这种不一致的方式(在此功能同时执行同步和异步操作)编写函数,因为这会使应用程序的行为无法预测。幸运的是,这些不一致可以很容易地修复如下:
functionwriteToMyFile(data,callback){ if(!data){ process.nextTick(()=>callback(newError('Nodataprovided'))); }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异步功能
但是,在撰写本文时,还无法使用纯JavaScript在libuv线程池上运行CPU密集型操作。但是,你可以编写自己的C++插件,使你能够安排libuv线程池上的工作。有某些第三方库(例如bcrypt),它们执行CPU密集型操作并使用C++插件来实现针对CPU绑定操作的异步API。
误解4-所有异步操作都在线程池上执行
现代操作系统具有内置的内核支持,可使用事件通知(例如,Linux中的epoll,macOS中的kqueue,Windows中的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的一个众所周知的事实,现在由于在Nodev10.5.0中引入WorkerThreads而被淘汰了。尽管它是作为实验性功能引入的,但worker_threads自Nodev12LTS起,该模块现已稳定,因此适合在具有CPU密集型操作的生产应用程序中使用。
每个Node.js工作线程将拥有其自己的v8运行时的副本,事件循环和libuv线程池。因此,执行阻塞CPU密集型操作的一个工作线程不会影响其他工作线程的事件循环,从而使它们可用于任何传入的工作。
但是,在撰写本文时,IDE对WorkerThreads的支持还不是最大。某些IDE不支持将调试器附加到在主线程以外的其他线程中运行的代码。但是,随着许多开发人员已经开始采用辅助线程进行CPU绑定的操作(例如视频编码等),开发支持将随着时间的推移而成熟。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。