
编者按
假如你是两个后端开发人员,你不懂像PHP、Python或Ruby等静态C语言,接着你想建立他们的服务项目,所以Node.js是两个十分好的优先选择。
Node.js 是运转在服务项目器端的 JavaScript,假如你熟识Javascript,所以你将会很难专业委员会Node.js。
总之,假如你是后端开发人员,想布署许多高效能的服务项目,所以自学Node.js也是两个十分好的优先选择。
全文6723字,预计今年写作天数17两分钟。
一、甚么是Node.js?
他们先看呵呵非官方对Node.js的表述:Node.js是两个如前所述V8 JavaScript发动机的JavaScript运转时自然环境。

但这句话可能将有点儿简略:
1、甚么是JavaScript运转自然环境?
2、为甚么JavaScript需要特别的运转自然环境呢?
3、甚么又是JavaScript发动机?
4、甚么是V8?
带着这些疑问他们先了解呵呵nodejs的历史,在 Node.js 出现之前,最常见的 JavaScript 运转时自然环境是浏览器,也叫做 JavaScript 的宿主自然环境。浏览器为 JavaScript 提供了 DOM API,能够让 JavaScript 操作浏览器自然环境(JS 自然环境)。
2009 年初 Node.js 出现了,它是如前所述 Chrome V8 发动机开发的 JavaScript 运转时自然环境,所以 Node.js 也是 JavaScript 的一种宿主自然环境。而它的底层就是他们所熟识的 Chrome 浏览器的 JavaScript 发动机,因此本质上和在 Chrome 浏览器中运转的 JavaScript 并没有甚么区别。但,Node.js 的运转自然环境和浏览器的运转自然环境还是不一样的。
通俗点讲,也就是说Node.js如前所述V8发动机来执行JavaScript的代码,但不仅仅只有V8发动机。
他们知道V8可以嵌入到任何C++应用程序中,无论是Chrome还是Node.js,事实上都是嵌入了V8发动机来执行JavaScript代码,但在Chrome浏览器中,还需要解析、渲染HTML、CSS等相关渲染发动机,另外还需要提供支持浏览器操作的API、浏览器他们的事件循环等。
另外,在Node.js中他们也需要进行许多额外的操作,比如文件系统读/写、网络IO、加密、压缩解压文件等操作。
所以接下来他们来看呵呵浏览器是如何解析渲染的。
二、浏览器是怎么渲染两个页面的?

浏览器渲染一个网页,简单来说可以分为以下几个步骤:
HTML 解析:在这个过程之前,浏览器会进行 DNS 解析及 TCP 握手等网络协议相关的操作,来与用户需要访问的域名服务项目器建议连接,域名服务项目器会给用户返回两个 HTML 文本用于后面的渲染 (这一点很关键,要注意)。
渲染树的构建:浏览器客户端在收到服务项目器端返回的 HTML 文本后,会对 HTML 的文本进行相关的解析,其中 DOM 会用于生成 DOM 树来决定页面的布局结构,CSS 则用于生成 CSSOM 树来决定页面元素的样式。假如在这个过程遇到脚本或是静态资源,会执行预加载对静态资源进行提前请求,最后将它们生成两个渲染树。

布局:浏览器在拿到渲染树后,会进行布局操作,来确定页面上每个对象的大小和位置,再进行渲染。
渲染:他们电脑的视图都是通过 GPU 的图像帧来显示出来的,渲染的过程其实就是将上面拿到的渲染树转化成 GPU 的图像帧来显示。首先,浏览器会根据布局树的位置进行栅格化(用过组件库的同学应该不陌生,就是把页面按行列分成对应的层,比如 12 栅格,根据对应的格列来确定位置),最后得到两个合成帧,包括文本、颜色、边框等;其次,将合成帧提升到 GPU 的图像帧,进而显示到页面中,就可以在电脑上看到他们的页面了。
相信看到这里,大家对浏览器怎么渲染出两个页面已经有了大致的了解。页面的绘制其实就是浏览器将 HTML 文本转化为对应页面帧的过程,页面的内容及渲染过程与第一步拿到的 HTML 文本是紧密相关的。
三、事件循环和异步IO
首先要了解事件循环是甚么?所以他们先了解进程和线上的概念。
进程和线程:都是操作系统的概念。
进程:计算机已经运转的程序,线程:操作系统能够调度的最小单位启动两个应用默认是开启两个进程(也可能将是多进程)。
每两个进程中都会启动两个线程用来执行程序中的代码,这个线程称为主线程。
举例子:比如工厂相当于操作系统,工厂里的车间相当于进程,车间里的工人相当于是线程,所以进程相当于是线程的容器。
所以浏览器是两个进程吗?它里面是只有两个线程吗?
目前浏览器一般都是多进程的,一般开启两个tab就会开启两个新的进程,这个是防止两个页面卡死而造成的所有页面都无法响应,整个浏览器需要强制退出。
其中每两个进程当中有包含了多个线程,其中包含了执行js代码的线程。
js代码是在两个单独的线程中执行的,单线程同一天数只能做一件事,假如这件事十分耗时,就意味着当前的线程会被阻塞,浏览器天数循环维护两个队列:宏任务队列和微任务队列。
宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等;
微任务队列(microtask queue):Promise的then回调。
所以事件循环对于两个队列的优先级是怎么样的呢?
main script中的代码优先执行(编写的顶层script代码);
在执行任何两个宏任务之前(不是队列,是两个宏任务),都会先查看微任务队列中是否有任务需要执行也就是宏任务执行之前,必须保证微任务队列是空的;
假如不为空,所以久优先执行微任务队列中的任务(回调)。
new promise()是同步,promise.then,promise.catch,resolve,reject 是微任务。
四、使用事件驱动程序
Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭接着进行处理,接着去服务项目下两个web请求。
当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
这个模型十分高效可扩展性十分强,因为webserver一直接受请求而不等待任何读写操作。(这也被称之为非阻塞式IO或者事件驱动IO)
在事件驱动模型中,会生成两个主循环来监听事件,当检测到事件时触发回调函数。

整个事件驱动的流程就是这么实现的,十分简洁。有点儿类似于观察者模式,事件相当于两个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。
Node.js 有多个内置的事件,他们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下实例:

以下程序绑定事件处理程序:

他们可以通过程序触发事件:

实例
建立 main.js 文件,代码如下所示:

接下来让他们执行以上代码:

五、Node.js架构以及与浏览器的区别

上图是 Node.js 的基本架构,他们可以看到,(Node.js 是运转在操作系统之上的),它底层由 V8 JavaScript 发动机,以及许多 C/C++ 写的库构成,包括 libUV 库、c-ares、llhttp/http-parser、open-ssl、zlib 等等。
其中,libUV 负责处理事件循环,c-ares、llhttp/http-parser、open-ssl、zlib 等库提供 DNS 解析、HTTP 协议、HTTPS 和文件压缩等功能。
在这些模块的上一层是中间层,中间层包括Node.js Bindings、Node.js Standard Library以及C/C++ AddOns。Node.js Bindings层的作用是将底层那些用 C/C++ 写的库接口暴露给 JS 自然环境,而Node.js Standard Library是 Node.js 本身的核心模块。至于C/C++ AddOns,它可以让用户他们的 C/C++ 模块通过桥接的方式提供给Node.js。
中间层之上就是 Node.js 的 API 层了,他们使用 Node.js 开发应用,主要是使用 Node.js 的 API 层,所以 Node.js 的应用最终就运转在 Node.js 的 API 层之上。
总结呵呵:Node.js 系统架构图,主要就是application、V8 javascript 发动机、Node.js bindings, libuv这4个部分组成的。
Application: nodejs应用,就是他们写的js代码。
V8: JavaScript 发动机,分析js代码后去调用Node api。
Node.js bindings:Node api,这些API最后由libuv驱动。
Libuv:异步I/O,实现异步非阻塞式的核心模块,libuv这个库提供两个最重要的东西是事件循环和线程池,两者共同构建了异步非阻塞I/O模型。
以线程为纬度来划分,可以分为Node.js线程和其他C++线程。
应用程序启动两个线程,在两个Node.js线程里完成,Node.js的I/O操作都是非阻塞式的,把大量的计算能力分发到其他的C++线程,C++线程完成计算后,再把结果回调到Node.js线程,Node.js线程再把内容返回给应用程序。

浏览器中的事件循环是根据HTML5规范来实现的,不同的浏览器可能将有不同的实现,而node中是libuv 实现的
因为 Node.js 不是浏览器,所以它不具有浏览器提供的 DOM API。
比如 Window 对象、Location 对象、Document 对象、HTMLElement 对象、Cookie 对象等等。
但,Node.js 提供了他们特有的 API,比如全局的 global 对象,
也提供了当前进程信息的 Process 对象,操作文件的 fs 模块,以及建立 Web 服务项目的 http 模块等等。这些 API 能够让他们使用 JavaScript 操作计算机,所以他们可以用 Node.js 平台开发 web 服务项目器。
也有许多对象是 Node.js 和浏览器共有的,如 JavaScript 发动机的内置对象,它们由 V8 发动机提供。常见的还有:
基本的常量 undefined、null、NaN、Infinity;
内置对象 Boolean、Number、String、Object、Symbol、Function、Array、Regexp、Set、Map、Promise、Proxy;
全局函数 eval、encodeURIComponent、decodeURIComponent等等。
此外,还有许多方法不属于发动机内置 API,但两者都能实现,比如 setTimeout、setInterval 方法,Console 对象等等。
5.1 阻塞IO和非阻塞IO
假如他们希望在程序中对两个文件进行操作,所以他们就需要打开这个文件:通过文件描述符。
他们思考:JavaScript可以直接对两个文件进行操作吗?
看起来是可以的,但事实上他们任何程序中的文件操作都是需要进行系统调用(操作系统的文件系统);事实上对文件的操作,是两个操作系统的IO操作(输入、输出)。
操作系统为他们提供了阻塞式调用和非阻塞式调用:
阻塞式调用: 调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配天数片的),调用线程只有在得到调用结果之后才会继续执行。
非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要过一段天数来检查呵呵有没有结果返回即可。
所以他们开发中的很多耗时操作,都可以如前所述这样的 非阻塞式调用:
比如网络请求本身使用了Socket通信,而Socket本身提供了select模型,可以进行非阻塞方式的工作;比如文件读写的IO操作,他们可以使用操作系统提供的如前所述事件的回调机制。5.2 非阻塞IO的问题
整的数据,他们需要频繁的去确定读取到的数据是否是完整的。
这个过程他们称之为轮训操作。

所以这个轮训的工作由谁来完成呢?假如他们的主线程频繁的去进行轮训的工作,所以必然会大大降低性能,并且开发中他们可能将不只是两个文件的读写,可能将是多个文件,而且可能将是多个功能:网络的IO、数据库的IO、子进程调用。libuv提供了两个线程池(Thread Pool):线程池会负责所有相关的操作,并且会通过轮训等方式等待
5.3 阻塞和非阻塞,同步和异步的区别?
首先阻塞和非阻塞是对于被调用者来说的;在他们这里就是系统调用,操作系统为他们提供了阻塞调用和非阻塞调用,同步和异步是对于调用者来说的。
在他们这里就是他们的程序;
假如他们在发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用;
假如他们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用。
Libuv采用的就是非阻塞异步IO的调用方式。
5.4 Node事件循环的阶段
他们最前面就强调过,事件循环像是两个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:
无论是他们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调
函数放到事件循环(任务队列)中;
事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;但一次完整的事件循环Tick分成很多个阶段:5.5 Node事件循环的阶段图解

六、Node.js常见的内置模块与全局变量


如想了解更多全局对象可参考以下链接 :https://m.runoob.com/nodejs/nodejs-global-object.html
——END——
参考资料:
[1]https://juejin.cn/post/6844903504931209224
[2]https://nodejs.org/zh-cn/docs/guides/