1.说明nodejs是单线程执行的,是事件驱动的非阻塞IO编程模型。这使我们可以继续执行代码,而无需等待异步操作的结果返回。当触发异步事件时,通知主线程,主线程执行相应事件的回调。本文讲解JavaScript代码在node.js中的执行过程。以下是测试代码。如果你知道输出结果,那么你就不需要再看这篇文章了。如果你不知道输出结果,那么这篇文章可以帮助你理解:console.log(1)setTimeout(function(){newPromise(function(resolve){console.log(2)resolve()})。then(()=>{console.log(3)})})setTimeout(function(){console.log(4)})复杂:setTimeout(()=>{console.log('1')newPromise((resolve)=>{console.log('2');resolve();}).then(()=>{console.log('3')})newPromise((resolve)=>{console.log('4');resolve()}).then(()=>{console.log('5')})setTimeout(()=>{console.log('6')setTimeout(()=>{console.log('7')newPromise((resolve)=>{console.log('8');resolve()}).then(()=>{console.log('9')})newPromise((resolve)=>{console.log('10');resolve()}).then(()=>{console.log('11')})})setTimeout(()=>{console.log('12')},0)})setTimeout(()=>{console.log('13')},0)})setTimeout(()=>{console.log('14')},0)newPromise((resolve)=>{console.log('15');resolve()}).then(()=>{console.log('16')})newPromise((resolve)=>{console.log('17');resolve()}).then(()=>{console.log('18')})2.的启动过程nodejsnode.js的启动过程可以分为以下几个步骤:调用platformInit方法,初始化nodejs运行环境,调用performance_node_start方法,对nodejs进行性能统计。openssl设置的判断。调用v8_platform.Initialize初始化libuv线程池。调用V8::Initialize初始化V8环境。创建一个nodejs运行实例。启动在上一步中创建的实例。开始执行js文件,执行完同步代码后进入事件循环。当没有事件可监听时,nodejs实例被销毁,程序执行完毕。三、nodejs的事件循环详解Nodejs将消息循环细分为6个阶段(官方称为Phase),每个阶段都会有一个类似队列的结构,里面存放着这个阶段需要处理的回调函数。Nodejs为了防止某个阶段任务过多,导致后续阶段饿死,所以在消息循环(iterate)的每次迭代中,每个阶段都有最大执行回调次数。若超过数量,则强制结束当前阶段,进入下一阶段。此规则适用于消息循环中的每个阶段。3.1Timer阶段这是消息循环的第一阶段,使用for循环处理所有的setTimeout和setInterval回调。这些回调存储在最小堆(minheap)中。这样引擎每次只需要判断head元素,满足条件就执行。它不会结束定时器阶段。Timer阶段判断某个回调是否合格,直到遇到不满足条件或者队列为空条件的方法也很简单。消息循环每次进入Timer时都会保存当时的系统时间,然后只要检查上面最小堆中回调函数设置的启动时间是否超过进入Timer时保存的时间即可。如果超过了,就拿出来执行。3.2PendingI/OCallback阶段执行除close回调、setTimeout()、setInterval()、setImmediate()回调之外的几乎所有回调,如TCP连接错误、fs.read、socket等IO操作回调函数还包括各种error回调。3.3Idle,准备系统内部的一些调用。3.4Poll阶段,重要阶段这是整个消息周期中最重要的阶段。它的作用是等待异步请求和数据,因为它支持整个消息循环机制。poll阶段主要有两个功能:一是执行下限时间到达定时器的回调,一是处理poll队列中的事件。注意:Node的很多API都是基于事件订阅的,比如fs.readFile,这些回调应该在poll阶段完成。当事件循环进入轮询阶段:当轮询队列不为空时,事件循环首先要遍历队列并同步执行回调,直到队列被清空或者回调执行次数达到系统上限。当轮询队列为空时,有两种情况。如果代码已经通过setImmediate()设置了回调,那么事件循环直接结束poll阶段,进入check阶段执行check队列中的回调。如果代码没有设置setImmediate()设置回调:如果设置了定时器,那么事件循环会在此时检查定时器,如果一个或多个定时器的下限时间已经到达,那么事件循环会回绕定时器阶段,并执行定时器的有效回调队列。如果不设置定时器,则事件循环阻塞在轮询阶段,等待此时事件回调加入轮询队列。在Poll阶段,当js层代码注册的事件回调都没有返回时,事件循环会暂时阻塞在poll阶段。解除阻塞的条件:执行poll阶段时,会传入一个timeout超时,超时时间为poll阶段的最大阻塞时间。当超时时间未到,如果有事件返回,则执行该事件注册的回调函数。当超时时间到时,退出轮询阶段并执行下一阶段。什么是适当的超时设置?答案是TimerPhase中最近一次要执行的回调开始时间到现在的差值,假设差值是delta。因为在PollPhase之后没有等待执行的回调。所以这里的最大等待时间是delta,如果在此期间有事件唤醒了消息循环,则继续下一个Phase的工作;如果期间没有任何反应,那么超时后,消息循环仍会进入后续的Phase,这样下一次迭代的TimerPhase也能得到Execution。Nodejs通过PollPhase驱动整个消息循环,等待IO事件和内核异步事件的到来。3.5Check阶段该阶段只处理setImmediate的回调函数。那么为什么会有一个特殊的阶段来处理setImmediate呢??简单的说就是因为在Poll阶段可能设置了一些回调,希望在Poll阶段之后运行。因此,在Poll阶段之后增加了Check阶段。3.6CloseCallbacks阶段专门处理一些close类型的回调。例如,socket.on('close',...)。用于资源清洗。四、Nodejs执行JS代码流程和事件循环流程1、节点初始化初始化节点环境,执行输入代码执行流程。nextTick回调执行微任务(microtasks)2、进入事件循环2.1、进入Timer阶段,检查是否有Timer队列中的expiredTimer回调,如果有则按照TimerId从小到大的顺序执行所有过期的Timer回调,检查是否有process.nextTick任务,如果有则执行all检查是否有microtasks(promise),如果有,全部执行退出本阶段2.2,进入PendingI/OCallback阶段,检查是否有PendingI/OCallback回调,如果有则执行回调。如果没有退出这个阶段,检查是否有process.nextTick任务。如果是,执行all检查是否有微任务(promise)。如果是,执行all退出该阶段。跳过2.4,进入Poll阶段,首先检查是否有未完成的回调,如果有,有如下两种情况:等)检查是否有process.nextTick任务,有则executeall检查是否有microtasks(promise),有则executeall退出本阶段第二种情况:无可执行回调检查是否有immediatecallback,如果是,退出Poll阶段。如果没有,则在此阶段阻塞并等待新的事件通知。如果没有pendingcallback,则退出Poll阶段2.5,进入check阶段。如果有立即回调,则执行所有立即回调,检查是否有process.nextTick任务。如果有,则Executeall检查是否有microtask(promise),如果有则executeall退出本阶段2.6,进入收尾阶段如果有立即回调,则执行所有立即回调,检查是否有进程。nextTicktask,如果有,则执行all,检查是否有Microtasks(promise),如果有,则全部执行,退出本阶段3.检查是否有activehandles(timer,IO等事件句柄),如果有,继续下一轮事件循环如果没有,则结束事件循环并退出程序注意:在事件循环的每个子阶段退出之前,将依次执行以下过程:检查是否有process.nextTick回调,以及如果是,则全部执行。检查是否有任何微任务(承诺),如果有,则全部执行。4.1关于Promise和process.nextTick事件循环队列首先保证所有process.nextTick回调,然后append所有Promise回调,最后在每个阶段结束时取出执行。另外,process.nextTick和Promise回调的次数是有限制的,也就是说,如果一直往这个队列中添加回调,整个事件循环就会卡住。4.2setTimeout(…,0)和setImmediate哪个回调方法更快?比如下面这个例子:setImmediate(()=>console.log(2))setTimeout(()=>console.log(1))使用nodejs多次执行后,发现有时输出是12,有时21。对于多次执行的不同输出结果,有必要了解事件循环的基本问题。首先,Nodejs启动,初始化环境后加载我们的JS代码(index.js)。发生了两件事(消息循环还没有进入):setImmediate添加回调到Check阶段console.log(2);setTimeout将回调console.log(1)添加到Timer阶段。此时初始化阶段完成,进入Nodejs消息循环。为什么有两个输出?下一步很关键:当执行到Timer阶段时,会有两种可能。因为每一轮迭代刚好进入Timer阶段,都会占用系统时间并保存,以ms(毫秒)为最小单位。如果Timer阶段的回调预置时间>消息循环中保存的时间,则在Timer阶段执行回调。在这种情况下,先输出1,然后输出2,直到执行完Check阶段。一般情况下,结果是12。如果操作快一些,Timer阶段回调的预置时间可能刚好等于消息循环中节省的时间。这种情况下Timer阶段的回调无法执行,继续下一阶段。直到Check阶段,输出2。然后等待Timer阶段的下一次迭代。此时的时间必须满足Timer阶段回调预设的时间>消息循环保存的时间,所以执行console.log(1)。输出1。一般情况下,结果是21。因此,输出不稳定的原因取决于进入Timer阶段的时间和执行setTimeout的时间是否在1ms以内。如果代码改成下面这样,肯定会稳定输出:require('fs').readFile('my-file-path.txt',()=>{setImmediate(()=>console.log(2))setTimeout(()=>console.log(1))});这是因为消息循环在PnedingI/OPhase中将回调插入到Timer和Check队列中。这时根据消息循环的执行顺序,Check必须在Timer之前执行。从性能上看,setTimeout是在TimerPhase中处理的,minheap保存了timer的回调,所以每次回调的执行都会涉及到heap的调整。SetImmediate只是清除队列。效率自然会高很多。在执行时机上,setTimeout(...,0)和setImmediate属于两个阶段。5.一个实际的例子演示下面的代码来说明nodejs运行JavaScript的机制。下面一段代码:setTimeout(()=>{//settimeout1console.log('1')newPromise((resolve)=>{console.log('2');resolve();})//Promise3.then(()=>{console.log('3')})newPromise((resolve)=>{console.log('4');resolve()})//Promise4.then(()=>{console.log('5')})setTimeout(()=>{//settimeout3console.log('6')setTimeout(()=>{//settimeout5console.log('7')newPromise((resolve)=>{console.log('8');resolve()})//Promise5.then(()=>{console.log('9')})newPromise((resolve)=>{console.log('10');resolve()})//Promise6.then(()=>{console.log('11')})})setTimeout(()=>{console.log('12')},0)//settimeout6})setTimeout(()=>{console.log('13')},0)//settimeout4})setTimeout(()=>{console.log('14')},0)//settimeout2newPromise((resolve)=>{console.log('15');resolve()})//承诺1。then(()=>{console.log('16')})newPromise((resolve)=>{console.log('17');resolve()})//Promise2.then(()=>{console.log('18')})以上代码执行过程:node初始化执行JavaScript代码遇到setTimeout,将回调函数放入Timer队列中,记录为settimeout1,遇到setTimeout,将回调函数放入Timer队列中,记录为settimeout2遇到Promise,执行,输出15,将回调函数放入microtask队列,记录为Promise1遇到Promise,执行,输出17,将回调函数放入microtask队列,记录为Promise2代码执行结束,该阶段输出结果:1517没有process.nextTick回调,跳过执行microtask查看microtask队列中是否有可执行的回调,此时有队列中有2个回调:Promise1和Promise2执行Promise1回调,输出16执行Promise2回调,输出18本阶段输出结果:1618进入第一个事件循环,进入Timer阶段,查看Timer队列是否有可执行回调。此时队列有2个回调:settimeout1,settimeout2执行settimeout1回调:output1,2,4新增2个microtask,记录为Promise3和Promise4,新增2个Timer任务,记录为settimeout3,settimeout4执行settimeout2回调,输出14Timer队列任务执行完成没有process.nextTick回调,跳过检查microtask队列是否可执行的回调,此时队列中有2个回调:Promise3,Promise4依次执行2个microtasks,输出3,5输出结果在此阶段:1241435PendingI/OCallback阶段没有任务,跳过进入Poll阶段检查是否有未完成的回调,此时有2个回调:settimeout3、settimeout4执行settimeout3回调输出6添加2个Timer任务,记录为settimeout5,settimeout6执行settimeout4回调,输出13没有process.nextTick回调,skipsnomicrotasks,跳过本阶段输出结果:613check,closingstage没有tasks,跳过检查是否有activehandle(timer,IOandotherevents)handle),yes,继续下一轮事件循环,进入第二次事件循环进入Timer阶段,检查Timer队列中是否有可执行的回调。此时队列有2个回调:settimeout5、settimeout6executesettimeout5callback:output7,8,10添加了2个microtasks,记录为Promise5,Promise6执行settimeout6回调,output12没有process.nextTick回调,跳过检查是否有是microtask队列中的可执行回调,此时队列有2个回调:Promise5、Promise6依次执行2个microtask,输出9、11本阶段输出结果:781012911PendingI/OCallback、Poll,检查,closing阶段没有任务,跳过检查是否有活动句柄(timer,IO等事件句柄),没有了,结束事件循环,退出程序,程序执行结束,输出结果:151716181241435613781012911参考资料深入剖析Node.js事件循环和消息队列浅析nodejs的事件循环NodeNode.js事件循环和异步APIEventLoopnodejs官方网站
