nodejs的结构如下图所示。通过v8引擎执行js代码,通过中间层libuv读写文件系统和网络做一些操作。Nodejs提供了阻塞和非阻塞的调用方式,比如在fs模块中读取文件,可以根据需要使用readFile(异步)或者readFileSync(同步)。如果使用同步编程方式,那么后续代码的执行需要等到本次执行结束,程序的运行就会“阻塞”。也可以使用异步编程,后续代码的执行不需要等待本次执行结束,不会“阻塞”程序的运行,但是它也有一个问题,就是非阻塞调用需要不断轮询获取异步调用的执行结果。libuv中的io操作使用“非阻塞调用”,但如果不断轮询获取结果,进程会对系统产生一定的性能影响。为了减少这种影响,libuv将不断轮询的过程放在了“线程池”中,当轮询到结果时,将相应的回调和得到的结果放在事件循环(eventloop)的某个队列中,并且事件循环进行下一步,通过javascript执行回调。nodejs中的事件循环比javascript中的事件循环更复杂。一个循环分为以下几个阶段*Timer(定时器):这个阶段会执行setTimeout和setInterval的回调函数*Pendingcallbacks:一些系统操作(比如TCP错误类型)执行回调*idle,prepare:only系统内部使用。*轮询(poll):检索新的I/O事件;执行I/O相关的回调(在几乎所有情况下,除了关闭的回调,那些由定时器和setImmediate()调度的回调)*检测(check):这里执行setImmediate()回调函数。*关闭回调(closecallbacks):一些关闭回调函数,如:socket.on('close',...)。在nodejs中,和javascript一样,也有微任务和宏任务。两个任务中执行的内容也有部分相似。微任务:promise的then函数的回调,queueMicrotask,process.nextTick宏任务:setTimeout,setInterval,io事件,setImmediate,close事件执行顺序也和javascript一致,先执行主线程的任务,然后执行microtask,microtask执行完成后执行macrotask,具体执行顺序如下。Microtaskqueuenexttickqueue:process.nextTickothertickqueue:promisethenfunction,queueMicrotask宏任务队列timerqueue:setTimeout,setIntervalpollqueue:ioeventcheckqueue:setImmediateclosequeue:closeevent了解了nodejs中事件循环的执行顺序后我们来看看下面的面试题asyncfunctionasync1(){console.log('async1start')awaitasync2()console.log('async1end')}asyncfunctionasync2(){console.log('async2')}console.log('脚本启动')setTimeout(function(){console.log('setTimeout0')},0)setTimeout(function(){console.log('setTimeout2')},300)setImmediate(()=>console.log('setImmediate'));process.nextTick(()=>console.log('nextTick1'));async1();process.nextTick(()=>console.log('nextTick2'));newPromise(function(resolve){console.log('promise1')resolve();console.log('promise2')}).then(function(){console.log('promise3')})console.log('scriptend')首先声明了async1和async2函数,只有调用会被放入调用栈,所以此时不??会执行,向下执行输出“scriptstart”。继续向下执行,将“setTimeout0”放入宏任务的定时器队列,继续执行,“setTimeout2”需要延时300ms,不放入定时器队列,再将“setImmediate”放入校验队列,将“nextTick1”放入下一个报价队列。此时执行函数async1,输出“async1start”,在函数async1中执行async2,输出“async2”,将“async1end”放入其他队列,此时,函数async1执行完毕,继续向下执行,将“nextTick2”放入下一个tick队列,在“nextTick1”之后,继续输出“promise1”,将“promise3”放入另一个队列,在“async1结束”之后,输出“promise2”,最后输出“脚本结束”至此,主线程的内容执行完毕。我们来到microtask队列,microtask队列先执行下一个tick队列中的内容,依次输出“nectTick1”和“nectTick2”,然后执行另一个队列中的内容,输出“async1end”和“promise3”的顺序。最后执行宏任务队列中的任务,先执行定时器队列,输出“setTimeout0”,没有io事件(poll),向下执行checkqueue,输出setImmediate,不要关闭回调函数阶段(closecallbasks),然后一个事件循环结束,来到第二个和第三个事件循环。300ms后,“setTimeout2”被添加到宏任务队列中的定时器队列中。事件循环中没有其他队列,直接输出“setTimeout2”,事件循环结束。简单图如下再看另一道面试题setTimeout(()=>{console.log("setTimeout");},0);setImmediate(()=>{console.log("setImmediate");});根据宏任务队列中各个任务的执行顺序,setTimeout属于定时器队列,setImmediate属于校验队列。按理说setTimeout会先输出,但是实际情况会怎样呢?让我们看看为什么会出现以下输出,有的时候先输出setTimeout,有的时候先输出setImmediate。原因是setTimeout的回调函数虽然延迟0毫秒执行,但是setTimeout的准备时间比事件循环的开始时间要长。当事件循环开始第一个循环的时候,setTimeout还没有被放入定时器队列,所以事件循环先执行check队列中的setImmediate,等待第二次循环时,定时器队列中只有setTimeout,然后setImmediate将首先输出。nodejs中的部分事件循环机制与javascript中的事件循环是一致的。如果你对javascript事件循环机制不熟悉,可以看这篇文章,javascript事件循环机制和面试题。
