Node.js 是一个JavaScript 运行平台,其显著特征是它的异步和事件驱动机制,以及小巧精悍的标准库。
Node 目前有两个活跃版本,长期支持版(LTS ) 和 当前版,由Node.js 基金会进行管理并提供支持。
官网地址:https://nodejs.org/en/
自2009年 Node.js问世以来,JavaScript渐渐变成了能开发所有软件的语言,其地位也越来越重要,不再是只能勉强在浏览器上用一下的鸡肋语言了。
这里有ECMAScript 2015的功劳,因为它解决了之前那些ECMAScript标准中遗留下来的几个关键问题。
Node所用的 Google V8引擎就是基于ECMAScript 2015开发的。ECMAScript 2015是 ECMAScript标准的第6个版本,所以有时也被称为ES6,一般简写为ES2015。Node、React和 Electron等技术创新成果的功劳也不可小觑,是它们让 JavaScript无处不在:从服务器到浏览器,到原生的移动端应用程序。甚至像微软这样的大公司都对JavaScript敞开了怀抱,也为Node的成功起到推波助澜的作用。
Node和JavaScript的优势之一是它们的单线程编程模型。
多个线程一般会引入bug,尽管一些新的编程语言,包括Go和Rust,试图提供更加安全的并发工具,但Node仍然保留了JavaScript 在浏览器中所用的模型。
在为浏览器编写代码时,我们写的指令序列一次执行一条,代码不是并行执行的。然而对于用户界面来说,这样是不合理的:没有哪个用户想在浏览器执行网络访问或文件获取这样的低速操作时干等着。
为了解决这个问题,浏览器引入了事件机制:
在点击按钮时,就有一个事件被触发,还有一个之前定义的函数会跑起来。这种机制可以规避一些在线程编程中经常出现的问题,比如资源死锁和竞态条件。
其实服务器端编程面对的情况也差不多:访问磁盘和网络这样的IO请求会比较慢,所以我们希望,在读取文件或通过网络发送消息时运行平台不会阻塞业务逻辑的执行。
Node用三种技术来解决这个问题:事件、异步API、非阻塞I/O。
在Node程序员看来,非阻塞IO是个底层术语。它的意思是说,程序可以在做其他事情时发起一个请求来获取网络资源,然后当网络操作完成时,将会运行一个回调函数来处理这个操作的结果。
下图展示了一个典型的Node Web应用程序,它用 Web应用库 Express来处理商店的订单流程。
为了购买产品,浏览器发起了一个请求,然后应用程序检查库存,为该用户创建一个账号,发回执邮件,并返回一个JSON HTTP响应给浏览器。同时在做的其他事情有:发送了一封回执邮件,更新了数据库来保存用户的详细信息和订单。代码本身很简单,就是 JavaScript指令,但运行平台是并发操作的,因为它使用了非阻塞IO。
在这个示意图中,数据库是通过网络访问的,Node中的网络访问是非阻塞的,它用了一个名为libuv的库来访问操作系统的非阻塞网络调用。
这个库在 Linux, macOS和 Windows中的实现是不同的,但不用担心,因为你只需要会用操作数据库的 JavaScript库就可以了。只要写一些db.insert (query,err =>{})这样的代码,Node就会帮你完成那些经过高度优化的非阻塞网络操作、
访问硬盘也差不多,但又不完全一样。在生成了回执邮件并从硬盘中读取邮件模板时,libuv借助线程池模拟出了一种使用非阻塞调用的假象。管理线程池是个苦差事,相较而言,email.send ( 'template.ejs ', (err,html) =>{})这样的代码肯定要容易理解得多了。
在进行速度较慢的处理时让Node能做其他事情,是使用带非阻塞IO的异步API真正的好处。即便你只有一个单线程、单进程的Node Web应用,它也可以同时处理上千个网站访客发起的连接。
在上面的应用程序中的“响应浏览器的请求”部分,在这个应用程序中,Node 内置的HTTP服务器库,即核心模块http.Server,负责用流、事件、Node的HTTP请求解析器的组合来处理请求,它是本地代码。
你用Express Web应用库添加的回调函数,也是由它触发的。这个回调函数又会触发数据库查询语句,最终应用程序会用HTTP发送JSON作为响应。
整个过程用了三个非阻塞网络调用:
①一个用于请求 ②一个用于数据库 ③还有一个用于响应。
Node是如何调配这些网络操作的呢?答案是事件轮询( event loop )。
下图展示了Node如何用事件轮询完成这三个网络操作。
事件轮询是单向运行的先入先出队列,它要经过几个阶段,轮询中每个迭代都要运行的重要阶段已经展示出来了。
首先是计时器开始执行,这些计时器都是用JavaScript函数setTimeout 和 setInterval安排好的。接下来是运行IO回调,即触发回调函数。轮询阶段会去获取新的I/O事件,最后是用setImmediate安排回调。
这是一个特例,因为它允许你将回调安排在当前队列中的I/O回调完成之后立即执行。
总之记住:
尽管Node是单线程的,但是我们仍然可以用它提供的工具写出可伸缩的高效代码。