AWebManCN

Menu

几个常见的 NodeJS 误区

一、NodeJS 是一门服务器语言

这个错误非常明显,NodeJS 是一个使用 Chrome V8 引擎运行 JavaScript 程序的运行时环境,正如 JRE (Java Runtime Environment) 是 Java 程序的运行环境一样,语言还是 JavaScript,和我们在浏览器中运行 JS 脚本没什么不同。区别在于,NodeJS 提供了一整套用于服务器编程(准确来说应该是除网页编程外)的工具包,例如处理网络连接的 net, http, https 模块,用于读写文件的 fs 模块等,以及 NPM 仓库中数十万的第三方模块。这些模块,加上 NodeJS 运行时,为 JavaScript 语言提供了跨平台、跨领域的编程能力。

除了服务器编程,NodeJS 还被广泛应用于客户端编程,如桌面软件(Electron 框架),手机软件(React-Native 框架,Apache Cordova 框架)等,当然还有现在特别火的“前端”开发,例如 React、Angular 和 Vue 常常使用 NodeJS 来搭建开发环境,用以提供网页的热重载功能。除此之外,还有一些不那么热门的领域,比如操作系统开发。你绝对没有看错,是操作系统,我自己也是最近才了解到,这是一个叫做 eXtern 的操作系统,不同于以前的 NodeOS 只有命令行界面,eXtern 是一个界面优美的现代化桌面操作系统,感兴趣的读者可以前往其官网进行了解。

除了 NodeJS 和浏览器,目前还有一些其他比较著名的软件为 JavaScript 提供运行环境,如著名的数据库软件 MongoDB(曾使用 V8 引擎,自 3.2 版本起使用 Mozilla 的 SpiderMonkey 引擎)。

二、JavaScript 是弱类型语言,不安全

实际上这句话并没有什么不对,只是有点过时。在现代应用中,特别是大型或企业应用,基本上不会直接简单地使用 JavaScript 来开发的。我们无法反驳这样的事实,JavaScript 是弱类型语言,没有编译期检查,无法确定代码是否存在错误。但现在,我们有更优秀地工具来做到这一点。

首先是 ESLint,它不但提供了开发时的变量和语法检查,还提供了编写代码的强制性规范,使得整体项目严谨、结构一致。编写 Python 和 Go 语言应用的人经常以他们的强制性规范为自豪,但实际上,JavaScript 也能做到这一点,并且更灵活,更优秀。项目的开发人员可以根据成员的大多数意见来统一标准,不但有严格的标准,还能够体现项目组的特色,按照自身需求和习惯来定制。A 组可以强制所有人使用双引号,B 组则可以更具自己的喜好来统一使用单引号。

然后是 Flow 框架和 TypeScript 语言,Flow 框架是由 Facebook 提出的 JavaScript 类型系统,它在弱类型的语言基础上加上了强类型声明,并随着 babel 和 React 而流行,目前已经成为前端开发中举足轻重的力量。而由微软公司开发的 TypeScript 语言则更胜一筹,不但为 JavaScript 添加了强类型系统,还提供了诸如 enum, interface, 泛型之类的特别类型,用以定义更复杂、更强大的数据结构。如果说 Flow 只是在 JavaScript 语言上进行类型系统强化,那么 TypeScript 则完全将其打造为了与 Java, C# 所匹敌的重量级编程语言,同时还保留着 JavaScript 的轻便和灵活特性。除了在前端有 Angular 框架最富盛名以外,TypeScript 在 NodeJS 服务端已经逐步取代了 JavaScript,成为大型应用的首选。

三、JavaScript 是解释型语言,速度慢

为什么说这句话也是错的?我们都知道 JavaScript 是弱类型的解释型语言,那肯定速度是比不上编译型语言的,甚至比不上编译为字节码(中间语言)的 Java。诚然,JavaScript 确实在执行速度上比不上它们,但 JavaScript 也绝不是简单的“解释型语言”。实际上这和运行 JavaScript 的引擎有关,至少在 NodeJS 所使用的 Chrome V8 引擎中,JavaScript 不只是解释运行。V8 引擎会在第一次执行 JavaScript 代码时对其进行“解释”,包括语法分析、创建 AST 树之类的,类似 Java,在这个过程完成之后,V8 引擎会将 JavaScript 代码编译成字节码,然后在虚拟机( V8 Virtual Machine)上运行,这也是为什么大家都说 V8 引擎优秀的地方,它极大地提高了 JavaScript 语言的执行速度,并且随着引擎地不断优化,和 Java 的差距也在越来越小。

下面的图片来自 Medium 网站文章 Understanding V8's Bytecode,该文章详细地介绍了 V8 引擎编译 JavaScript 代码的细节,希望想深入了解其内核的读者,我非常建议去阅读一下它。

当然为了迎合不想花时间阅读或者对阅读英语文章有困难的读者,我也在这里简单概括一下它里面的内容。

首先,如大多数接触过 Java 的用户一样,第一句话是:

Bytecode is an abstraction of machine code. 字节码是机器码的一种抽象。

V8 引擎定义了数百个用来存储 JavaScript 对象信息和运算操作的字节码符号,当程序运行的时候,这些字节码被转换为机器码,即可立即在 CPU 上运行。因为字节码在转换成机器码的过程是非常快的,这要比解释高级语言快得多,因此经过了 V8 引擎编译的 JavaScript 代码要比传统的解释器执行快出许多。开发者可以直接查看 V8 引擎编译后的字节码是什么样子的,只需要在 node 命令后加上 --print-bytecode 参数来执行 JavaScript 程序文件即可。

四、NodeJS 是单进程单线程的,性能低下

这个误区虽然很显然,但如果不仔细考察,甚至很多已经熟悉 NodeJS 的开发者也会简单地认为这个误区在“性能低下”上。实际上,这个误区包含两个部分。对 NodeJS 性能的质疑可以很容易解释为什么它是错的。首先,NodeJS 使用异步事件驱动,专为高并发设计。如果你还不了解异步和事件驱动是什么概念,那么可以参考一下 Nginx。众所周知 Nginx 是一个高性能的反向代理服务器,同时也是一个高性能的静态文件伺服器、负载均衡器等等。

那么 Nginx 是怎么做到高性能的呢?是的,它使用异步和事件驱动。可能多数人不了解 Nginx 这个特点大概是因为它仅仅只是一个服务器软件,接触比较多的就是它的配置,而没有真正去了解过它的实现。NodeJS 之所以也使用异步和事件循环,很大程度上是参考了 Nginx,唯一不同的地方,NodeJS 使用了 Libuv 库,而 Nginx 则使用自己实现的库。不过不要怀疑这两种实现具有多大的区别,它们基本逻辑是一样的,并且也都使用 C 语言编写。

既然说到了 Libuv,就不得不谈这个误区中最严重的部分,如果说“性能”的迟疑大家还好理解并更正,那么“单线程”这个问题就比较吃力了。首先需要说明的是 NodeJS 绝不是单线程的。“单线程”指的是运行在 NodeJS 环境中的 JavaScript 代码,即应用层的部分,但底层,用来处理网络连接、文件读写等等所有涉及 I/O 的操作,都是多线程的。Libuv 库使用了一个非常高效的线程池,来在不同时刻或“同一时刻”处理 I/O,并将接口以异步回调的方式暴露给应用层的 JavaScript 代码,从而实现异步无阻塞。

Node.js runs JavaScript code in the Event Loop (initialization and callbacks), and offers a Worker Pool to handle expensive tasks like file I/O.
Node.js 在事件循环中运行 JavaScript 代码(初始化和回调),并提供一个工作线程池来处理耗时的任务,如 I/O。

这句话摘自 NodeJS 官网的 Don't Block the Event Loop (or the Worker Pool) 章节,里面详细介绍了事件循环和线程池相关的内容,感兴趣的读者可以自行前往查阅,我在这里就不再重复描述了,仅点到为止。

至于单进程,这个不能说是不对的,绝大多数服务器程序默认都是单进程的(我所知道的 swoole 除外),在 NodeJS 中,虽然程序默认单进程运行,但可以通过 cluster(集群)模块来开启多进程,从而获取全部 CPU 核心的运算能力。就如很多人喜欢用的那一句“压榨 CPU”,通过集群,NodeJS 服务器可以成倍地提升网络吞吐量。

五、NodeJS 异步回调难看、不具备真协程

首先我们不得不承认异步回调代码在大多数时候很难看,流程控制很难编写,但这不是 JavaScript 语言才具有的毛病,也不是 NodeJS 才具有的毛病,任何一门语言,特别是现在还不支持协程的 Java,只要用到异步,回调函数总是成为语言的一个痛处。

不过更需要知道,现在已经基本上不会有人再去写复杂难看的异步回调函数了,JavaScript 社区先是提出了 Promise 来解决大多数回调函数的使用问题,然后又最终引入了 async/await 协程,对于熟悉 C# 和 Python 的读者,这两个关键字一定耳熟能详、津津乐道。它将复杂的异步逻辑,使用同步的、更符合人类习惯的同步风格来编写应用程序代码,不但大大减少了开发失误,还使代码变得更简洁、更美观。这一切,你都可以在 NodeJS 7.6 版本以后享受到,对于此前的版本,则可以使用 co 框架临时解决或使用 babel 和 TypeScript 转译代码实现。

另一个不明真相的争议是,指责 JavaScript 中的 async/await 是“伪协程”,其中一大部分责难来自 Go 语言社区,因为 Go 使用了不同的 Goroutine 机制。我不想在这里讨论谁才是真协程或谁才是假协程,仅从 Coroutine 的定义来讲解 JavaScript 中,以及 C#,Python 等语言(具体支持 Coroutine 的语言列表可以查阅维基百科)中的 async/await 是如何定义协程的。

协程和线程类似,它又叫轻量级线程(或用户态线程,区别于系统级线程),主要依靠生成器来实现,并且和生成器函数(及 yield 语法)具有相同的原理。yield 的作用,是在函数运行的过程中,先“暂停”其运行,将其状态保存起来并让出 CPU 控制权,使 CPU 先执行其他的过程,等控制权转让最后回到了当前程序暂停的地方,继续运行,从而使程序能够在单线程中持续切换运行的代码块,而不会因为某一部分代码阻塞导致程序其他部分被挂起而无法执行。

和线程一样的,协程只在单个进程内存在,并且它共享内存状态。所谓的共享内存状态最简单的表现就是当 await 一个异步函数的返回值时,和获取一个普通函数的返回值是一样的,不会因为协程的切换而导致数据的内存状态丢失。从这一点上,如果已经熟悉的 Goroutine 的读者就会很明显地发现其中的不同,Coroutine 满足这一设定,而 Goroutine 则不满足。出于这个原因,当我们一般提到协程的时候,指的就是 Coroutine,因此协程是 Coroutine 的概念,Goroutine 则是另外一套东西。

需要注意的是,NodeJS 在 6.0 版本以后,也就是 ECMAScript 2015 标准中才支持生成器,此前的版本有的可以使用 harmony 模式来执行,而 babel 和 TypeScript 编译为 ES5 的代码只能通过模拟来实现。当然是不推荐使用模拟实现的,因为其效率比较低。另外,V8 引擎对生成器具有特别的优化,因此直接使用 async/await 语法是最好的。V8 引擎中用来处理生成器的字节码符号为 SuspendGenerator,感兴趣的读者可以自行查阅了解。

这篇文章仅列出这几个我认为比较重要、亟待指出的 NodeJS 误区,但实际上,就经历来看,包括我自己在内,都还在其他地方存在着其它的误区,但由于时间和精力的关系,这篇文章无法面面俱到,仅以此作为结尾,各种对编程语言和平台的偏见,还需要更多的时间和磨练才能够完全消除。

— 于 共写了4873个字
— 文内使用到的标签:

发表评论

电子邮件地址不会被公开。 必填项已用*标注