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

Node.js事件周期工作流程&生命周期

时间:2023-04-03 16:17:57 Node.js

本文将详细讲解节点。Javascript引擎的一部分(V8、spiderMonkey等)。实际上,事件循环主要是利用Javascript引擎来执行代码。有一个堆栈或队列。首先,没有堆栈。其次,流程复杂,有多个队列(如数据结构中的队列)参与。但是大多数开发人员都知道有多少回调函数被压入单个队列,这是完全错误的。事件循环在单独的线程中运行由于错误的node.js事件循环图,我们中的一些人认为你有两个线程。一个执行Javascript,另一个执行事件循环。事实上,它们都在一个线程中运行。setTimeout涉及具有异步操作系统的系统的另一个非常大的误解是,在给定的延迟完成后,setTimeout回调被(可能由操作系统或内核)推送到队列中。setImmediate将回调函数放在第一位,因为常见的事件循环描述只有一个队列;所以一些开发人员认为setImmediate将回调放在工作队列的前面。这是完全错误的,Javascript中的工作队列是先进先出的。事件循环的架构在我们开始描述事件循环的工作流程之前,了解它的架构很重要。下图展示了事件循环的真实工作流程:图中不同的方框代表不同的阶段,每个阶段执行特定的工作。每个阶段都有一个队列(这里叫队列主要是为了更好理解,真正的数据结构不一定是队列),Javascript可以在任何阶段执行(idle&prepare除外)。还可以看到图中的nextTickQueue和microTaskQueue,它们不是循环的一部分,里面的回调可以在任意阶段执行。他们有更高的执行优先级。现在你知道事件循环是不同阶段和不同队列的组合;下面是每个阶段的描述。定时器(Timer)阶段这是事件循环开始的阶段,绑定到这个阶段的队列中,保留了定时器的回调(setTimeout,setInterval),虽然没有将回调推入队列,但是与最小堆维护一个计时器,并在到达指定事件时执行回调。挂起(Pending)I/O回调阶段该阶段执行事件循环中pending_queue中的回调,这些回调是之前的操作推送的。例如,当您尝试向tcp写入内容时,工作已完成并且回调被推送到队列中。错误处理的回调也在这里。Idle,Prepare阶段在每个tick运行,尽管名称是空闲的。Prepare也在轮询阶段开始之前运行。反正这两个阶段是node主要做一些内部操作的阶段。轮询阶段也许整个事件循环中最重要的阶段是轮询阶段。此阶段接受新的传入连接(新的Socket建立等)和数据(文件读取等)。我们可以将轮询阶段分为几个不同的部分。如果watch_queue(poll阶段绑定的队列)中有item,就会一个接一个的执行,直到队列为空或者系统达到最大限制。一旦队列为空,节点就会等待新的连接。等待或休眠的事件取决于多种因素。检查(Check)阶段轮询的下一个阶段是检查阶段,专门用于setImmediate。为什么需要专门的队列来处理setImmediate回调?这是因为轮询阶段的行为,这将在稍后的流程部分中讨论。现在请记住,检查阶段主要处理setImmediate()回调。关闭(Close)回调回调(stock.on('close',()=>{}))的关闭在这里处理,更像是一个清理阶段。nextTickQueueµTaskQueuennextTickQueue中的任务保留在process.nextTick()触发的回调中。microTaskQueue保存由Promises触发的回调。它们都不是事件循环的一部分(不是在libUV中开发的),而是在node.js中。C/C++和Javascript有交集的地方,尽可能快地调用它们。所以它们应该在当前操作之后执行(不一定是当前的js回调)。事件循环工作流程在控制台中运行nodemy-script.js时,node会设置事件循环,然后在事件循环之外运行主模块(my-script.js)。一旦主模块执行完毕,节点将检查循环是否仍然存在(事件循环中是否还有其他事情要做)?如果没有,将在执行退出回调后退出。process,on('exit',foo)回调(退出回调)。但是如果循环是存活的,节点将从定时器阶段进入循环。定时器阶段(Timerphase)的工作流事件循环进入定时器阶段,检查定时器队列中是否有需要执行的东西。嗯,这句话听起来很简单,但是事件循环实际上执行了一些步骤来找到合适的回调。实际上,定时器脚本是按升序存储在堆内存中的。它首先获取一个执行定时器,计算是否now-registeredTime==delta?如果是,则执行定时器回调并检查下一个定时器。直到它找到一个没有预定时间的定时器,它停止检查其他定时器(因为定时器都是按升序排列的)并进入下一阶段。假设你调用了4次setTimeout创建了4个定时器,相对于时间t分别是100、200、300、400。假设事件循环在t+250进入定时器阶段。它会首先查看定时器A,其到期时间为t+100。但是现在时间是t+250。所以它会执行定时器A上绑定的回调。然后查看定时器B,发现它的超时时间是t+200,所以B的回调也会被执行。现在它检查C,看到它的到期时间是t+300,然后离开它。时间循环不检查D,因为计时器是按升序排列的;因此D具有比C更高的阈值。但是这个阶段有一个系统相关的硬限制,如果达到系统相关的最大限制数量,即使有未执行的计时器,它也会进入下一阶段。在pending(Pengding阶段)I/O阶段workflow定时器阶段之后,事件循环会进入pendingI/O阶段,然后检查pending_queue中是否有之前pending任务的回调。如果有,则一个一个执行,直到队列为空,或者达到系统的最大限制。之后,事件循环将进入空闲处理程序阶段,然后是准备阶段进行一些内部操作。然后它可能最终进入最重要的阶段,即轮询阶段。轮询阶段(Pollphase)工作流程顾名思义,这是一个观察阶段。观察传入的请求或连接。当事件循环进入轮询阶段时,会像其他阶段一样执行watcher_queue中的脚本,包括文件读取响应、新的socket或http连接请求,直到事件耗尽或达到系统依赖限制。假设没有要执行的回调,轮询器将在特定条件下等待一段时间。如果检查队列、挂起队列或关闭回调队列或空闲处理程序队列中有任何任务在等待,它将等待0毫秒。然后它根据定时器堆决定执行第一个定时器(如果可用)的等待时间。如果第一个定时器阈值通过,毫无疑问不需要等待(第一个定时器会被执行)。检查阶段(Checkphase)在工作流的轮询阶段之后,立即进入检查阶段。这个阶段队列中有apisetImmediate触发的回调。它将像其他阶段一样一个接一个地执行,直到队列为空或达到系统相关的最大限制。关闭回调(Closecallback)的工作流完成检查阶段的任务后,事件循环的下一个目的地就是处理关闭或销毁类型的回调关闭回调。在事件循环执行完该阶段队列中的回调后,它会检查循环是否仍然存在,如果不存在,则退出。但如果还有工作要做,则进入下一个循环;因此定时器阶段。如果你认为前面例子中的定时器(A&B)已经过期,现在定时器阶段将从定时器C开始检查它是??否过期。nextTickQueueµTaskQueue那么,这两个队列的回调函数什么时候运行呢?在从当前阶段进入下一阶段之前,它们当然会尽可能快地运行。与其他阶段不同,它们都没有系统相关的耗尽限制,并且节点运行它们直到两个队列都为空。但是,nextTickQueue的任务优先级会高于microTaskQueue。进程池(Thread-pool)我从Javascript开发者那里听到的一个常用词就是ThreadPool。一个常见的误解是nodejs有一个处理所有异步操作的进程池。但实际上,进程池在libUV(nodejs用来处理异步的第三方库)库中。它没有画在图中的原因是因为它不是循环机制的一部分。目前,并非每个异步任务都由进程池处理。libUV能够灵活地使用操作系统的异步api来保持环境事件驱动。但是操作系统的api不能做文件读取,dns查询等,这些都是由进程池处理的,默认只有4个进程。您可以通过设置uv_threadpool_size环境变量将进程数增加到128。带有示例的工作流程希望您能理解事件循环的工作原理。C中的同步while有助于Javascript异步。一次只处理一件事,但它非常阻塞。当然,无论我们如何描述理论,最好通过示例来理解,所以让我们通过一些代码片段来理解这个脚本。片段1——基本理解setTimeout(()=>{console.log('setTimeout');},0);setImmediate(()=>{console.log('setImmediate');});你能猜出上面的输出吗?好吧,你可能认为setTimeout会先被打印出来,但这并不能保证,为什么呢?执行主模块后进入定时器阶段,他可能不会或者会发现你的定时器耗尽了。为什么?根据系统时间和您提供的增量时间注册计时器脚本。计时器脚本与setTimeout调用同时写入内存,可能会有一小段延迟,具体取决于您机器的性能和在其上运行的其他操作(非节点)。还有一点,node在进入timer阶段(每一轮遍历)之前只设置一个变量now,使用now作为当前时间。所以你可以说确切的时间有点问题。这就是不确定性的原因。如果您在计时器代码回调中指向相同的代码,您会得到相同的结果。但是,如果将此代码移入i/o周期,则可以保证setImmediate回调将在setTimeout之前运行。fs.readFile('my-file-path.txt',()=>{setTimeout(()=>{console.log('setTimeout');},0);setImmediate(()=>{console.log('setImmediate');});});片段2—更好地理解定时器vari=0;varstart=newDate();函数foo(){i++;如果(i<1000){setImmediate(foo);}else{varend=newDate();console.log("执行时间:",(end-start));}}foo();上面的例子非常简单。在函数内部调用函数foo,通过setImmediate递归调用foo直到1000。在我的电脑上,大概需要6到8毫秒。仙女,修改上面的代码,把setImmedaite(foo)换成setTimeout(foo,o)。变种我=0;varstart=newDate();函数foo(){i++;如果(i<1000){setTimeout(foo,0);}else{varend=newDate();console.log("执行时间:",(end-start));}}foo();现在在我的电脑上运行这段代码需要1400+ms。为什么?他们都没有i/o事件,所以他们应该是一样的。上面两个例子等待事件为0,为什么要这么久?通过事件比较发现的偏差,CPU密集型任务,需要更多时间。注册定时器脚本也消耗事件。定时器的每个阶段都需要一些操作来确定定时器是否应该执行。更长的执行时间也会导致更多的滴答声。但是在setImmediate中,只有一个stage需要检查,好像是在一个队列中,然后执行。片段3—了解nextTick()和定时器执行vari=0;functionfoo(){i++;如果(i>20)返回;console.log("foo");setTimeout(()=>console.log("setTimeout"),0);process.nextTick(foo);}setTimeout(foo,2000);你认为上面的输出是什么?是的,它会打印foo然后setTimeout。2秒后,nextTickQueue递归调用foo()打印出第一个foo。当nextTickQueues全部执行完,开始执行其他的(比如setTimeout回调)。那么每次回调执行完之后,就开始检查nextTickQueue?让我们更改代码看看。vari=0;functionfoo(){i++;如果(i>20)返回;console.log("foo");setTimeout(()=>console.log("setTimeout"),0);process.nextTick(foo);}setTimeout(foo,2000);setTimeout(()=>{console.log("OthersetTimeout");},2000);在setTimeout之后,我只是添加了另一个具有相同延迟的输出OthersetTimeout'ssetTimeout。虽然不能保证,但有可能在输出第一个foo之后,OthersetTimeout会输出。同一个定时器分成一组,nextTickQueue会在正在进行的回调组执行完后执行。一些一般性问题Javascript代码在哪里执行?就像我们大多数人认为事件循环是在一个单独的线程中,将回调推入队列,然后一个接一个地执行。初次阅读本文的读者可能会疑惑,Javascript是在哪里执行的?正如我之前所说,只有一个线程运行使用V8或其他引擎的事件循环本身的Javascript代码。执行是同步的,如果当前Javascript执行尚未完成,事件循环将不会传播。我们有setTimeout(fn,0),为什么还需要setImmediate?首先,它不是0,而是1。当你设置一个定时器,时间小于1,或者大于2147483647ms,它会自动设置为1。所以如果你设置setTimeout的延迟时间为0,它将自动设置为1。此外,setImmediate减少了额外的检查。所以setImmediate会执行得更快。它也被放置在轮询阶段之后,因此来自setImmediate回调的任何传入请求都将立即执行。为什么理解会调用setImmediate?setImmediate和process.nextTick()都命名错误。所以从功能上来说,setImmediate在下一次报价时执行,nextTick立即执行。Javascript代码会被屏蔽吗?由于nextTickQueue没有回调执行限制。所以如果你递归地执行process.nextTick(),你的程序可能永远不会脱离事件循环,无论你在其他阶段有什么。如果我在退出回调阶段调用setTimeout会发生什么?它可能会初始化计时器,但可能永远不会调用回调。因为如果node在退出回调阶段,就已经跳出事件循环了。所以没有回去执行。一些简短的结论事件循环没有工作堆栈事件循环不在单独的线程中,Javascript执行不像从队列中弹出回调执行那样简单。setImmediate不会将回调推送到工作队列的头部,有专门的阶段和队列。setImmediate是在下一个循环执行的,nextTick其实是立即执行的。当心,如果递归调用nextTickQueue可能会阻塞您的节点代码。