百度工程师带你体验引擎中的nodejs

作者 | 糖果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运转时环境。

百度工程师带你体验引擎中的nodejs

可是这句话可能有点笼统:

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 浏览器是怎样烘托一个页面的?

百度工程师带你体验引擎中的nodejs

浏览器烘托一个网页,简略来说能够分为以下几个进程:

  • HTML 解析:在这个进程之前,浏览器会进行 DNS 解析及 TCP 握手等网络协议相关的操作,来与用户需求拜访的域名服务器建议衔接,域名服务器会给用户回来一个 HTML 文本用于后面的烘托 (这一点很要害,要注意)。

  • 烘托树的构建:浏览器客户端在收到服务端回来的 HTML 文本后,会对 HTML 的文本进行相关的解析,其间 DOM 会用于生成 DOM 树来决议页面的布局结构,CSS 则用于生成 CSSOM 树来决议页面元素的样式。假如在这个进程遇到脚本或是静态资源,会履行预加载对静态资源进行提前恳求,最后将它们生成一个烘托树。

百度工程师带你体验引擎中的nodejs

  • 布局:浏览器在拿到烘托树后,会进行布局操作,来确认页面上每个目标的巨细和方位,再进行烘托。

  • 烘托:咱们电脑的视图都是经过 GPU 的图画帧来显现出来的,烘托的进程其实便是将上面拿到的烘托树转化成 GPU 的图画帧来显现。

    首要,浏览器会依据布局树的方位进行栅格化(用过组件库的同学应该不生疏,便是把页面按队伍分红对应的层,比方 12 栅格,依据对应的格列来确认方位),最后得到一个组成帧,包含文本、色彩、边框等;其次,将组成帧提升到 GPU 的图画帧,从而显现到页面中,就能够在电脑上看到咱们的页面了。

信任看到这儿,大家对浏览器怎样烘托出一个页面现已有了大致的了解。页面的制作其实便是浏览器将 HTML 文本转化为对应页面帧的进程,页面的内容及烘托进程与第一步拿到的 HTML 文本是严密相关的。

03 事情循环和异步IO

首要要了解事情循环是什么?那么咱们先了解进程和线上的概念。

  • 进程和线程:都是操作体系的概念。

  • 进程:核算机现已运转的程序,线程:操作体系能够调度的最小单位发动一个运用默许是敞开一个进程(也可能是多进程)。

每一个进程中都会发动一个线程用来履行程序中的代码,这个线程称为主线程。

举比方:比方工厂适当于操作体系,工厂里的车间适当于进程,车间里的工人适当所以线程,所以进程适当所以线程的容器。

那么浏览器是一个进程吗?它里边是只要一个线程吗?

目前浏览器一般都是多进程的,一般敞开一个tab就会敞开一个新的进程,这个是避免一个页面卡死而形成的一切页面都无法响应,整个浏览器需求强制退出。

其间每一个进程当中有包含了多个线程,其间包含了履行js代码的线程。

js代码是在一个单独的线程中履行的,单线程同一时刻只能做一件事,假如这件事十分耗时,就意味着当时的线程会被堵塞,浏览器时刻循环保护两个行列:宏使命行列和微使命行列。

  • 宏使命行列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等;

  • 微使命行列(microtask queue):Promise的then回调。

那么事情循环关于两个行列的优先级是怎样样的呢?

  1. main script中的代码优先履行(编写的顶层script代码);

  2. 在履行任何一个宏使命之前(不是行列,是一个宏使命),都会先检查微使命行列中是否有使命需求履行也便是宏使命履行之前,必须确保微使命行列是空的;

  3. 假如不为空,那么久优先履行微使命行列中的使命(回调)。

new promise()是同步,promise.then,promise.catch,resolve,reject 是微使命。

04 运用事情驱动程序

Node.js 运用事情驱动模型,当web server接纳到恳求,就把它封闭然后进行处理,然后去服务下一个web恳求。

当这个恳求完结,它被放回处理行列,当抵达行列最初,这个成果被回来给用户。

这个模型十分高效可扩展性十分强,由于webserver一直接受恳求而不等候任何读写操作。(这也被称之为非堵塞式IO或者事情驱动IO)

在事情驱动模型中,会生成一个主循环来监听事情,当检测到事情时触发回调函数。

百度工程师带你体验引擎中的nodejs

整个事情驱动的流程便是这么完结的,十分简练。有点类似于观察者形式,事情适当于一个主题(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架构以及与浏览器的差异

百度工程师带你体验引擎中的nodejs

上图是 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线程再把内容回来给运用程序。

百度工程师带你体验引擎中的nodejs

浏览器中的事情循环是依据HTML5规范来完结的,不同的浏览器可能有不同的完结,而node中是libuv 完结的

由于 Node.js 不是浏览器,所以它不具有浏览器供给的 DOM API。

  1. 比方 Window 目标、Location 目标、Document 目标、HTMLElement 目标、Cookie 目标等等。

  2. 可是,Node.js 供给了自己特有的 API,比方大局的 global 目标,

  3. 也供给了当时进程信息的 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也会存在一定的问题:咱们并没有获取到需求读取(咱们以读取为例)的成果,那么就意味着为了能够知道是否读取到了完好的数据,咱们需求频频的去确认读取到的数据是否是完好的。

这个进程咱们称之为轮训操作。

百度工程师带你体验引擎中的nodejs

那么这个轮训的作业由谁来完结呢?
假如咱们的主线程频频的去进行轮训的作业,那么必然会大大降低性能,而且开发中咱们可能不只是一个文件的读写,可能是多个文件,而且可能是多个功能:网络的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事情循环的阶段图解

百度工程师带你体验引擎中的nodejs

06 Node.js常见的内置模块与大局变量

百度工程师带你体验引擎中的nodejs

百度工程师带你体验引擎中的nodejs

如想了解更多大局目标可参考以下链接 :m.runoob.com/nodejs/node…

——END——

参考资料:

[1]/post/684490…

[2]nodejs.org/zh-cn/docs/…

[3]部分图片来源于稀土掘金网站

引荐阅览

揭秘百度智能测试在测试定位领域实践

百度工程师带你探秘C++内存管理(ptmalloc篇)

为什么 OpenCV 核算的视频 FPS 是错的

百度 Android 直播秒开体验优化

iOS SIGKILL 信号量崩溃抓取以及优化实践

如安在几百万qps的网关服务中完结灵敏调度策略