共计 6630 个字符,预计需要花费 17 分钟才能阅读完成。
作者 | 糖果 candy
导读
如果你是一个前端程序员,你不懂得像 PHP、Python 或 Ruby 等动静编程语言,而后你想创立本人的服务,那么 Node.js 是一个十分好的抉择。
Node.js 是运行在服务端的 JavaScript,如果你相熟 Javascript,那么你将会很容易学会 Node.js。
当然,如果你是后端程序员,想部署一些高性能的服务,那么学习 Node.js 也是一个十分好的抉择。
全文 6723 字,预计浏览工夫 17 分钟。
01 什么是 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、加密、压缩解压文件等操作。
那么接下来咱们来看一下浏览器是如何解析渲染的。
02 浏览器是怎么渲染一个页面的?
浏览器渲染一个网页,简略来说能够分为以下几个步骤:
- HTML 解析:在这个过程之前,浏览器会进行 DNS 解析及 TCP 握手等网络协议相干的操作,来与用户须要拜访的域名服务器倡议连贯,域名服务器会给用户返回一个 HTML 文本用于前面的渲染(这一点很要害,要留神)。
- 渲染树的构建:浏览器客户端在收到服务端返回的 HTML 文本后,会对 HTML 的文本进行相干的解析,其中 DOM 会用于生成 DOM 树来决定页面的布局构造,CSS 则用于生成 CSSOM 树来决定页面元素的款式。如果在这个过程遇到脚本或是动态资源,会执行预加载对动态资源进行提前申请,最初将它们生成一个渲染树。
- 布局:浏览器在拿到渲染树后,会进行布局操作,来确定页面上每个对象的大小和地位,再进行渲染。
-
渲染:咱们电脑的视图都是通过 GPU 的图像帧来显示进去的,渲染的过程其实就是将下面拿到的渲染树转化成 GPU 的图像帧来显示。
首先,浏览器会依据布局树的地位进行栅格化(用过组件库的同学应该不生疏,就是把页面按行列分成对应的层,比方 12 栅格,依据对应的格列来确定地位),最初失去一个合成帧,包含文本、色彩、边框等;其次,将合成帧晋升到 GPU 的图像帧,进而显示到页面中,就能够在电脑上看到咱们的页面了。
置信看到这里,大家对浏览器怎么渲染出一个页面曾经有了大抵的理解。页面的绘制其实就是浏览器将 HTML 文本转化为对应页面帧的过程,页面的内容及渲染过程与第一步拿到的 HTML 文本是严密相干的。
03 事件循环和异步 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 是微工作。
04 应用事件驱动程序
Node.js 应用事件驱动模型,当 web server 接管到申请,就把它敞开而后进行解决,而后去服务下一个 web 申请。
当这个申请实现,它被放回解决队列,当达到队列结尾,这个后果被返回给用户。
这个模型十分高效可扩展性十分强,因为 webserver 始终承受申请而不期待任何读写操作。(这也被称之为非阻塞式 IO 或者事件驱动 IO)
在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。
整个事件驱动的流程就是这么实现的,十分简洁。有点相似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。
Node.js 有多个内置的事件,咱们能够通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下实例:
// 引入 events 模块
var events = require('events');
// 创立 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
以下程序绑定事件处理程序:
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);
咱们能够通过程序触发事件:
// 触发事件
eventEmitter.emit('eventName');
实例
创立 main.js 文件,代码如下所示:
// 引入 events 模块 var events = require('events');
// 创立 eventEmitter 对象 var eventEmitter = new events.EventEmitter();
// 创立事件处理程序 var connectHandler = functionconnected() {console.log('连贯胜利~~~');
// 触发 data_received 事件
eventEmitter.emit('data_received');
}
// 绑定 connection 事件处理程序
eventEmitter.on('connection', connectHandler);
// 应用匿名函数绑定 data_received 事件
eventEmitter.on('data_received', function(){console.log('数据接管结束。');
});
// 触发 connection 事件
eventEmitter.emit('connection');
console.log("程序执行结束。");
接下来让咱们执行以上代码:
$ node main.js
连贯胜利~~~
数据接管结束。程序执行结束。
05 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、数据库的 IO、子过程调用。
libuv 提供了一个线程池(Thread Pool):线程池会负责所有相干的操作,并且会通过轮训等形式期待后果。当获取到后果时,就能够将对应的回调放到事件循环(某一个事件队列)中。事件循环就能够负责接管后续的回调工作,告知 JavaScript 应用程序执行对应的回调函数。
5.3 阻塞和非阻塞,同步和异步的区别?
首先阻塞和非阻塞是对于被调用者来说的;在咱们这里就是零碎调用,操作系统为咱们提供了阻塞调用和非阻塞调用,同步和异步是对于调用者来说的。
- 在咱们这里就是本人的程序;
- 如果咱们在发动调用之后,不会进行其余任何的操作,只是期待后果,这个过程就称之为同步调用;
- 如果咱们再发动调用之后,并不会期待后果,持续实现其余的工作,等到有回调时再去执行,这个过程就是异步调用。
Libuv 采纳的就是非阻塞异步 IO 的调用形式。
5.4 Node 事件循环的阶段
咱们最后面就强调过,事件循环像是一个桥梁,是连贯着应用程序的 JavaScript 和零碎调用之间的通道:
无论是咱们的文件 IO、数据库、网络 IO、定时器、子过程,在实现对应的操作后,都会将对应的后果和回调
函数放到事件循环(工作队列)中;
事件循环会一直的从工作队列中取出对应的事件(回调函数)来执行;
然而一次残缺的事件循环 Tick 分成很多个阶段:
定时器(Timers):本阶段执行曾经被 setTimeout() 和 setInterval() 的调度回调函数。
待定回调(Pending Callback):对某些零碎操作(如 TCP 谬误类型)执行回调,比方 TCP 连贯时接管idle, prepare:仅零碎外部应用。
轮询(Poll):检索新的 I/O 事件;执行与 I/O 相干的回调;
检测 :setImmediate() 回调函数在这里执行。
敞开的回调函数:一些敞开的回调函数,如:socket.on(‘close’, …)。
5.5 Node 事件循环的阶段图解
06 Node.js 常见的内置模块与全局变量
如想理解更多全局对象可参考以下链接:https://m.runoob.com/nodejs/n…
——END——
参考资料:
[1]https://juejin.cn/post/684490…
[2]https://nodejs.org/zh-cn/docs…
[3]局部图片来源于稀土掘金网站
举荐浏览:
揭秘百度智能测试在测试定位畛域实际
百度工程师带你探秘 C ++ 内存治理(ptmalloc 篇)
为什么 OpenCV 计算的视频 FPS 是错的
百度 Android 直播秒开体验优化
iOS SIGKILL 信号量解体抓取以及优化实际
如何在几百万 qps 的网关服务中实现灵便调度策略