当前位置: 首页 > 后端技术 > Node.js

深入学习Node.js(一)——基本概念

时间:2023-04-03 15:56:28 Node.js

前言现在Node.js对于前端来说越来越重要,笔者现在也觉得学好Node.js可以大大提升前端的竞争力。之前也多多少少接触过,但是没有系统的学习过程,所以打算接下来系统地学习Node.js,希望能创建一个从零到一学习Node的博客。希望我的学习经历可以帮到你。2020年Node.js无所不包,大家对Node肯定有一个初步的了解,但我还是想介绍一下Node是什么。Node.js是一个基于ChromeV8引擎的JavaScript运行环境(runtime)。Node不是一门允许js运行在后端runtime上的语言,也不包含全套的javascript,因为DOM和BOM不包含在server端。Node还提供了一些新的模块,如http、fs模块等。Node.js使用事件驱动、非阻塞I/O模型使其轻量、高效。而Node.js的包管理器npm是世界上最大的开源库生态系统。为什么要用来学习Node.js(一)首先,Node在处理高并发和IO密集型场景上优势明显。高并发指的是并发访问服务器IO,即同时输入和输出,指的是文件读写、网络操作、数据库操作等。相比之下,它是CPU密集型的,也就是说逻辑处理和传输比较频繁,一般指解密、加密、压缩、解压等操作。(2)其次,现在前端开发越来越离不开Node。日常工作中使用的webpack和cli脚手架工具都是Node编写的。前端LearningNode将大大增加你的话语权,提高你的竞争力。异步非阻塞IO传统语言,比如,大多使用同步阻塞,即当我读取一个文件的内容时,必须等待文件的所有内容都读取完才能进行下面的内容,所以整个线程会停止执行造成阻塞,大大降低执行效率。由于Node.js采用异步非阻塞模型,当我读取一个文件时,下面的代码会立即执行。当读取文件时,处理结果会放入我们的回调函数中,提高了执行效率。在阻塞模式下,一个线程只能处理一个IO。如果要并发处理多个任务,只能开多线程。在非阻塞模式下,一个线程可以处理多个IO,CPU利用率可以最大化。单线程Node.js采用单线程模式,同样适用于单线程非阻塞模式。当Node.js需要处理IO时,会把任务交给内部的libuv。libuv处理完成后,会通过IO事件驱动机制将结果返回给Node.js线程。事件驱动每次IO操作完成,都会触发相应的事件。由于Node.js是单线程的,一次只能处理一个任务,多余的任务只能放在队列中等待调用。为了处理这个过程,Node.js采用了事件循环策略,它类似于浏览器的事件循环,但也有很多不同之处。事件循环当Node.js启动时,它会初始化事件循环,处理提供的输入脚本,它可能会调用一些异步API,调度一个定时器,或者调用process.nextTick(),然后开始处理事件循环。下面是来自官网的事件循环的描述图。事件循环官网说的很清楚,我就把官网的内容搬过来这里。注意:每个框称为事件循环机制的一个阶段。每个阶段都有一个FIFO队列来执行回调。虽然每个阶段都是特殊的,但一般来说,当事件循环进入给定阶段时,它会执行该阶段特定的任何操作,然后执行该阶段队列中的回调,直到队列耗尽或执行回调的最大次数。当队列耗尽或达到回调限制时,事件循环将进入下一阶段,依此类推。由于这些操作中的任何一个都可能在轮询阶段调度更多操作和内核排队的新事件来处理,并且轮询事件可能在处理轮询中的事件时排队。因此,长时间运行的回调可以允许轮询阶段运行的时间超过计时器的阈值时间。PhaseOverviewTimer:这个阶段执行已经setTimeout()和setInterval()的调度回调函数。挂起的回调:I/O回调,其执行被推迟到下一个循环迭代。空闲,准备:仅供系统内部使用。轮询(poll):检索新的I/O事件;执行I/O相关的回调(几乎在所有情况下,除了关闭的回调函数,那些由定时器和setImmediate()调度的),其余节点会在适当的时候阻塞在这里。检查:这里执行了setImmediate()回调函数。关闭回调:一些关闭回调,如:socket.on('close',...)。在事件循环的每次运行之间,Node.js检查它是否正在等待任何异步I/O或计时器,如果没有,则完全关闭。阶段的详细概述计时器计时器指定可以执行提供的回调的阈值,而不是用户希望它执行的确切时间。在指定的时间间隔后,定时器回调将尽可能早地运行。但是,操作系统调度或其他正在运行的回调可能会延迟它们。注意:轮询阶段控制计时器何时执行。例如,假设您安排一个计时器在100毫秒后超时,然后您的脚本开始异步读取一个需要95毫秒的文件:constfs=require('fs');functionsomeAsyncOperation(callback){//假设这需要95毫秒才能完成完成fs.readFile('/path/to/file',callback);}consttimeoutScheduled=Date.now();setTimeout(()=>{constdelay=Date.now()-timeoutScheduled;console.log(`${delay}ms已经过去了,因为我被安排了`);},100);//做一些需要95毫秒才能完成的AsyncOperationsomeAsyncOperation(()=>{conststartCallback=Date.now();//做一些会花费10毫秒...while(Date.now()-startCallback<10){//什么都不做}});当事件循环进入轮询阶段时,它有一个空队列(此时fs.readFile()还没有完成),所以它等待剩余的毫秒数,直到达到最快的一个定时器阈值。当它等待95毫秒过去时,fs.readFile()完成读取文件,其回调(需要10毫秒完成)被添加到轮询队列并执行。当回调完成后,队列中不再有回调,因此事件循环机制会寻找最快达到阈值的定时器,然后回落到定时器阶段执行定时器的回调。在此示例中,您将看到分派计时器和执行其回调之间的总延迟为105毫秒。注意:为了防止轮询阶段使事件循环挨饿,libuv(实现Node.js事件循环和平台所有异步行为的C库)也有一个硬最大值(取决于系统)。Pendingcallbacks(挂起的回调)这个阶段执行某些系统操作的回调,例如TCP错误类型。例如,如果TCP套接字在尝试连接时收到ECONNREFUSED,一些*nix系统希望等待报告错误。这将在挂起的回调阶段排队等待执行。轮询轮询阶段有两个重要功能:计算何时应该阻塞和轮询I/O。然后,处理轮询队列中的事件。当事件循环进入轮询阶段并且没有定时器被调度时,会发生以下两种情况之一:如果轮询队列不为空,事件循环将遍历回调队列并同步执行它们直到队列耗尽,或者已达到与系统相关的硬限制。如果轮询队列为空,还会发生两件事:如果脚本由setImmediate()调度,事件循环将结束轮询阶段,并继续检查阶段以执行那些调度的脚本。如果脚本没有被setImmediate()调度,事件循环会等待回调被添加到队列中,然后立即执行。一旦轮询队列为空,事件循环就会检查已达到时间阈值的计时器。如果一个或多个计时器准备就绪,事件循环将返回到计时器阶段以执行这些计时器的回调检查阶段此阶段允许在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲并且脚本在使用setImmediate()后排队,事件循环可能会继续检查阶段而不是等待。setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuvAPI来安排回调在轮询阶段完成后执行。通常,在执行代码时,事件循环最终会进入轮询阶段,等待传入的连接、请求等。但是,如果回调已经用setImmediate()调度,并且poll阶段变为空闲,它会结束这个阶段并继续检查阶段,而不是继续等待poll事件。以上事件循环流程内容来自官网Node.jsMicrotasks熟悉浏览器事件循环的都知道,每执行一次macrotask,就会执行microtask队列中的任务。同样,Node.js也有微任务,只不过它有两个微任务队列,process.nextTick和Promise。每个阶段执行完后,process.nextTick队列和promise队列中的任务都会被执行。process.nextTick的优先级高于promise。Promise.resolve().then(()=>console.log('promise'))process.nextTick(()=>console.log('nextTick'))//输出结果//nextTick//promise上面是Node.js的一些基本概念。