Node.js 平台介绍
Node.js 最有特点的是它的异步特性,这使得它的代码风格大量使用回调(callback
)和 promise
。
Node.js 的另一个特型是它自身的准则。
Node.js 准则
Node.js 准则,也就是以 “符合Node 的方式” 来设计、编写代码。
核心功能小
Node.js 的核心(core)— 可以理解为 Node.js 的运行时和内置模块,它有自己的构建原则。
- 拥有尽可能少的功能集,将更多的空间留给社区和使用者。
尽可能小的模块
Node.js 使用模块(module)作为构建程序代码的基本手段。Node.js 其中一项原则就是设计小的模块和包。
依赖于包管理工具 — 例如 npm
、yarn
等,它可以程序依赖大量小的、聚焦特定功能模块,而不会产生冲突。
小的模块、包具有一些优点:
- 可重用性;
- 易理解、使用;
- 易于测试和维护;
- 尺寸小,有利于在浏览器使用。
更小的接口
在Node.js中,定义模块的一个非常常见的模式是只公开一个功能,比如一个函数或一个类,因为它提供了一个单一的、明确无误的入口点。
Node.js 工作原理
I/O 操作很慢
I/O 操作,也就是基本的输入输出操作,它是计算机基本操作中最慢的。它在请求发送到设备以及操作完成的时刻都增加了延迟。另外还要考虑操作人的因素,例如点击鼠标、打字输入等操作。
I/O 阻塞
在传统的阻塞 I/O 编程中,调用 I/O 请求的函数会阻塞当前线程,直到 I/O 操作完成。
例如一个 Web 服务器,如果使用阻塞 I/O 的方式编写,那么无法在一个线程上同时处理多个请求。需要使用多线程来分别处理不同的请求。不幸的是,就系统资源而言,线程并不便宜——它消耗内存并导致上下文切换——因此,为每个连接设置一个长时间运行的线程,并且在大多数时间不使用它意味着浪费宝贵的内存和CPU。
非阻塞 I/O
在非阻塞 I/O模式下,系统调用总是立即返回,不必等待数据被读取或写入。如果在调用时没有可用的结果,则该函数将简单地返回一个预定义的常量,表明此时没有可用的数据。
处理非阻塞 I/O 的最基本模式是在循环中主动轮询资源,直到返回一些实际数据。这就是所谓的 bysy-waiting(忙等待)。
事件解复用
忙等待的方式不是最佳的处理非阻塞 I/O 的方式。大多数现代操作系统都提供了另一种方式,叫做 synchronous event demultiplexer
(同步事件解复用器),也可称为 event notification interface
(事件通知接口)。
在电信中,多路复用(multiplexing)是指将多个信号组合为一个信号,这样可以在容量有限的介质中传播。解复用(Demultiplexing)指的是相反的操作,即信号再次被分割成其原始分量。
同步事件解复用器会监视多个资源,并在对其中一个资源执行读或写操作完成时返回一个新事件(或一组事件)。下面是一段伪代码:
watchedList.add(socketA, FOR_READ)
watchedList.add(fileB, FOR_READ)
while (events = demultiplexer.watch(watchedList)) {
// 事件循环
for (event of events) {
// 读取不会阻塞,会马上返回 data
data = event.resource.read()
if (data === RESOURCE_CLOSED) {
// 资源已经关闭,从监控列表中移除
demultiplexer.unwatch(event.resource)
} else {
// 获取到了实际的数据,进行处理
consumeData(data)
}
}
}
使用上面的模式就可以在单个线程中处理多个 I/O 操作。这么做的优势是最小化线程的空闲时间。
反应器模式
反应器模式(The reactor pattern)的主要思想是与每一个 I/O 模式关联一个处理函数。这个处理函数在 Node.js 中就是一个回调(callback)。
反应器模式的处理流程如下图所示:
- 应用程序向 事件解复用器(Event Demultiplexer) 提交一个请求来生成一个新的 I/O 操作。同时应用程序指定一个 处理函数(Execute Handler),当操作完成的时候调用。向事件解复用器提交一个请求是非阻塞的,它会立刻将控制权返回给应用程序。
- 当一系列的 I/O 操作完成之后,事件解复用器会将一系列的事件推入 事件队列(Event Queue)。
- 此时,事件循环会遍历事件队列中的项。
- 对于每一个事件,相关联的处理函数会调用。
- 处理函数是应用程序代码的一部分,当它处理完成之后会叫控制权交还给事件循环(5a)。当处理程序执行时,它可以请求新的异步操作(5b),导致新项目被添加到事件解复用器(1)。
- 当处理完事件队列中的所有项后,事件循环再次阻塞事件解复用器,当有新事件可用时,事件解复用器触发另一个循环。
异步处理已经比较清晰了。应用程序在某个时间点(没有阻塞的)访问资源,并提供一个处理程序,当操作完成时,将在另一个时间点调用该处理程序。
当事件解复用器没有更多要处理的操作,事件循环没有待处理的事件时候,Node.js 应用程序将会退出。
Node.js 的 I/O 处理引擎 Libuv
Node.js 核心团队创建了一个名为 libuv
的本地库,目的是使 Node.js 与所有主要的操作系统兼容,并规范不同类型资源的非阻塞行为。 libuv
是 Node.js 的底层的 I/O 引擎,是 Node.js 的重要组成。
除了抽象底层系统调用之外,libuv
还实现了反应器模式,从而为创建事件循环、管理事件队列、运行异步 I/O 操作以及为其他类型的任务排队提供了 API。
Node.js 的组成
Node.js 的组成:
- 反应器模式;
- libuv;
- 一组绑定(Bindings),负责将 libuv 和其他底层功能暴露给 JavaScript;
- V8 引擎;
- 一组 JavaScript 实现的核心库,用来实现 Node.js API。
Node.js 中的 JavaScript
Node.js 中的 JavaScript 和浏览器中有所不同。它没有 DOM
,也没有 window
和 document
。另外,Node.js 可以访问底层系统提供的一些服务,这些在浏览器中也没有。
放心使用最新的 JavaScript 特性
在 Node.js 中不需要考虑不同的浏览器所造成的特性不支持。因为我们明确知道代码运行的系统和 Node.js 的版本。同时 Node.js 附带了最新版本的 V8,意味着可以不需要额外的编译即可使用最新的 ECMAScript 版本。
模块系统
Node.js 自带模块系统,最初的模块系统是 CommonJS
。
目前,JavaScript 有了 ES Module。Node.js 继承了相同的语法,但是底层实现略有不同。
可实现操作系统的完全访问
Node.js为底层操作系统提供的所有主要服务提供绑定。例如 fs
模块访问文件系统;使用net
和 dgram
模块,我们可以编写使用低级 TCP 或 UDP 套接字的应用程序,等等。
运行原生代码
提供了 N-API
接口,可以访问本机的 C/C++ 代码。
虽然 V8 处理 JavaScript 非常快,但是对于 CPU 计算密集型的任务,交给原生代码更合适。