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

news2024/12/29 10:12:43

在这里插入图片描述

作者 | 糖果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回调。

那么事件循环对于两个队列的优先级是怎么样的呢?

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

  2. 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行也就是宏任务执行之前,必须保证微任务队列是空的;

  3. 如果不为空,那么久优先执行微任务队列中的任务(回调)。

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。

  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也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果,那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的。

这个过程我们称之为轮训操作。

图片

那么这个轮训的工作由谁来完成呢?
如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能,并且开发中我们可能不只是一个文件的读写,可能是多个文件,而且可能是多个功能:网络的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/nodejs-global-object.html

——END——

参考资料:

[1]https://juejin.cn/post/6844903504931209224

[2]https://nodejs.org/zh-cn/docs/guides/

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

推荐阅读

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

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

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

百度 Android 直播秒开体验优化

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

如何在几百万qps的网关服务中实现灵活调度策略

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/108408.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kotlin 协程】Flow 异步流 ③ ( 冷流 | 流被收集时运行 | 流的连续性 )

文章目录一、冷流 ( 流被收集时运行 )二、流的连续性一、冷流 ( 流被收集时运行 ) Flow 异步流 的 构建器函数 flow 函数 中的 代码 , 在 调用 Flow#collect 函数 时 , 也就是在 Flow 异步流 收集元素时 , 才会 执行 flow 构建器 中的代码 ; 这种机制的异步流 称为 冷流 ; 代…

移动WEB开发之响应式布局--Bootstrap栅格系统

栅格系统简介 栅格系统英文为“grid systems”,也有人翻译为“网格系统”,它是指将页面布局划分为等宽的列,然后通过列数 的定义来模块化页面布局。 Bootstrap 提供了一套响应式、移动设备优先的流式栅格系统,随着屏幕或视口(vi…

spring-statemachine状态机梳理

目录 一、基本回顾 1、为什么要用状态机 2、什么是状态机 3、状态机可归纳为4个要素 4、对应Spring StateMachine的核心步骤 5、简单例子 添加maven依赖 定义状态枚举和事件枚举 完成状态机的配置 简单测试一下 添加Listener 监听器,当状态变更时,触发方…

1. SpringMVC概述与入门

1. SpringMVC简介 SpringMVC是一种基于Java实现MVC模型的轻量级Web框架优点 使用简单,开发便捷(相比于Servlet)灵活性强 2. 入门案例 2.1 实现步骤分析 1 创建web工程(Maven结构) 2 设置tomcat服务器,加…

Qt属性系统(Qt Property System)

Qt提供了巧妙的属性系统,它与某些编译器支持的属性系统相似。然而,作为平台和编译器无关的库,Qt不能够依赖于那些非标准的编译器特性,比如__property 或者 [property]。Qt的解决方案能够被任何Qt支持的平台下的标准C编译器支持。它…

Kafka工作流程简介

消息传递模式简介: 一个消息系统负责将数据从一个应用程序传递到另外一个应用程序中,应用程序只关注数据,无需关注数据在多个应用之间是如何传递的。 分布式消息传递基于可靠的消息队列,在客户端应用和消息系统之间异步传递消息。 消息传递有…

MySQL面试常问问题(日志) —— 赶快收藏

目录 1.MySQL日志文件有哪些?分别介绍下作用? 2.binlog和redo log有什么区别? 3.一条更新语句怎么执行的了解吗? 4.那为什么要两阶段提交呢? 5.redo log怎么刷入磁盘的知道吗? 1.MySQL日志文件有哪些&…

Typora配合PicGo阿里云图床配置

写博客的时候,刚开始直接在各大平台上直接写,后来还是觉得不太方便,需要在各大平台之间来回切换。于是就改用Typora,但是有个问题就是图片的处理,只能放在本地。想要发布到各大平台,就需要图床。本文结合阿…

2022年安徽最新水利水电施工安全员模拟试题及答案

百分百题库提供水利水电施工安全员考试试题、水利水电施工安全员考试预测题、水利水电施工安全员考试真题、水利水电施工安全员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 15.围堰工程可以直接判定为生产安全重大事故隐…

【UE4 第一人称射击游戏】02-玩家健康和护甲

步骤: 1.在“ThirdPersonCharacter”中添加两个浮点变量“Health”和“Armor” 将默认值设为1.0,表示默认100% 2.新建一个控件蓝图,命名为“FPSHUD” 打开“FPSHUD”,添加两个进度条,分别表示当前的生命值和护甲量 调…

InnoDB架构体系

2、InnoDB架构体系 2.1、内存结构 2.1.1、buffer pool InnoDB内存缓存区,使用空间换时间的思想,给数据做了一个缓存。把热点的数据存储在内存中,减少IO次数,提高效率。 show variables like %innodb_buffer_pool%;buffer pool …

基于polar码和SCMA的多用户检测的联合检测译码matlab仿真,polar采用SCAN软译码,SCMA用MPA算法

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 构造的核心是通过信道极化(channel polarization)处理,在编码侧采用方法使各个子信道呈现出不同的可靠性,当码长持续增加时,部分信道将…

【C语言】初阶习题

目录 1.图案问题 2、时分秒转换 3、打印1-100之间所有3的倍数的数字 4、打印100~200之间的素数 5、给定两个数,求这两个数的最大公约数 6、在屏幕上输出9*9乘法口诀表 7、计算1/1-1/21/3-1/41/5 …… 1/99 - 1/100 的值,打印出结果 8、二分查找 …

说明DBCO-PEG-SH二苯并环辛炔-聚乙二醇-巯基科研试剂材料,DBCO-PEG-SH结构式

结构式 英文:DBCO-PEG-SH,DBCO-PEG-Thiol 中文:二苯并环辛炔-聚乙二醇-巯基 溶剂:溶于水、DMSO等常规有机溶剂 性状:液体/固体白色或淡黄色粉末,取决于分子量 用途范围:广泛应用于医药、生物…

一文搞懂Linux下并制作环形缓冲区

1.环形缓冲区log_buf[]又是存在内核的哪个文件呢? 位于/proc/kmsg里,所以除了dmesg命令查看,也可以使用cat /proc/kmsg来查看 2.但是,dmesg命令和cat /proc/kmsg有所不同 2.1 dmesg命令 每次使用,都会打印出环形缓冲区的所有信息 2.2 cat /proc/kmsg 只会打印…

项目管理中,进度计划是摆设吗?

1、忽视进度计划 项目管理中,有的人认为进度计划是摆设,不重视计划,只是为了满足合同工期,做给客户看,因此草率的编制进度计划。 在通过客户要求后,就将计划搁置在一边,这就导致后期在执行任务…

科班演员陶奕菱亮相海南电影节:人生没有白走的路,每一步都算数

今天的记忆是带着海水的味道………”12月18日,在年末岁尾之际,徽风皖韵熏陶下成长的新生代演员陶奕菱再次来到海南三亚,受邀参加第四届海南岛国际电影节,迫不及待地跑向沙滩面朝大海,感受多姿多彩的魅力三亚。 优雅端…

Java对象内存布局和对象头

对象的内存布局 在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头 对象头由对象标记Mark W…

进程间通信

进程间通信 进程间通信是不同进程之间的信息传输或交换。在不同的过程中,双方可以访问哪些媒体?进程的用户空间相互独立。一般来说,他们不能互相接触。唯一的例外是共享内存区域。此外,系统空间是一个“公共场所”,所…

【OFDM系列8】对知乎“正交频分复用(OFDM)原理灵魂9问”的理解与通俗易懂回答(慎入,含大量重要公式详细推导的万字长文)

前段时间,在知乎上看到一篇文章: 正交频分复用(OFDM)原理 文中博主提出了关于OFDM的九个问题,看了之后感觉这些问题的确深入OFDM本质,仔细思考可以很好地加深对OFDM这种较为复杂的调制方式的理解,下面谈一谈本人粗浅的一些理解和粗浅的认识,如有不准确或不恰当之处,欢…