1中的事件循环和异步API。介绍单线程编程会因为阻塞I/O导致硬件资源得不到最佳利用。多线程编程也会因为编程中的死锁、状态同步等问题而让开发者头疼。Node给出了介于两者之间的解决方案:使用单线程避免多线程死锁和状态同步等问题;使用异步I/O让单线程远离阻塞,从而更好地利用CPU。实际上node在应用层只是单线程的,底层其实通过libuv维护了一个阻塞I/O调用的线程池。但是:在应用层,JS是单线程的,业务代码中不能有耗时代码,否则可能会严重延迟后续代码(包括回调)的处理。如果遇到复杂的业务计算,应该想办法启用一个独立的进程或者交给其他服务处理。1.1异步I/O在Node中,JS是单线程执行的,但是内部I/O工作还有另外一个线程池,使用一个主进程和多个I/O线程来模拟异步I/O。当主线程发起I/O调用时,I/O操作会在I/O线程上执行,主线程会继续执行后面的任务。I/O线程完成操作后,会用数据通知主线程发起回调。1.2事件循环事件循环是Node的执行模型,正是这种模型使得回调函数如此普遍。当进程启动时,Node会创建一个类似于while(true)的循环。每次循环执行的过程就是判断是否有未决事件。如果是,则取出事件及其相关的回调并执行,然后进入下一个循环。如果没有更多事件要处理,则退出该过程。事件循环是一种程序结构,是一种实现异步的机制。事件循环可以简单理解为:所有任务都在主线程上执行,形成一个执行上下文栈。除了主线程之外,还有一个“任务队列”。系统将异步任务放入“任务队列”,然后主线程继续执行后续任务。一旦“执行堆栈”中的所有任务都已执行,系统将读取“任务队列”。如果此时,异步任务已经结束等待状态,就会从“任务队列”进入执行栈,重新开始执行。主线程不断重复上面的第三步。Node中事件循环阶段分析:┌──────────────────────────┐┌─>│定时器││└──────────────┬──────────────┘│┌────────────┴──────────────────────────────────┐││I/O回调││└──────────────────────────┘│┌────────────────────┐││空闲,准备││└────────┬────────────────────────────┐│┌────────────┴────────────────┐┐传入:│││投票│<──────┤连接,││└────────────┬──────────────┘│资料等└────────────────────┘││检查││└────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘││检查││└──────────────────────────────────────────────┘│┌──────────────┴──────────────┐└──┤收盘回调│└──────────────────────────┘每个stage都有一个FIFO回调队列(queue)去执行。每个阶段都有自己的特点。简单的说,当事件循环进入到某个阶段时,会执行该阶段的特定(任意)操作,然后执行该阶段队列中的回调。当队列执行完,或者执行的回调数达到上限,事件循环就会进入下一阶段。PhasesOverviewPhasesOverview定时器:这个阶段执行由setTimeout()和setInterval()设置的回调。I/O回调:执行几乎所有回调,除了关闭回调、setTimeout()、setInterval()、setImmediate()回调。空闲,准备:仅供内部使用。poll:获取新的I/O事件;节点会在合适的条件下阻塞在这里。check:执行setImmediate()设置的回调。关闭回调:执行回调,例如socket.on('close',...)。1.timers定时器指定一个下限时间而不是确切的时间,定时器setTimeout()和setInterval()在到达下限时间后执行回调。定时器在指定时间过去后尽可能早地执行回调,但系统调度或其他回调执行可能会延迟它们。从技术上讲,poll阶段控制timer何时执行,具体执行位置在timer中。下限时间有一个范围:[1,2147483647],如果设置的时间不在这个范围内,则设置为1。2.I/O回调执行除了close回调,setTimeout(),setInterval(),setImmediate()几乎所有的回调,除了回调,比如TCP连接错误。3.空闲,准备一些系统内部的调用。4.poll这是最复杂的阶段。poll将检索新的I/O事件,并在适当的时候阻塞,等待回调被添加。poll阶段主要有两个作用:一个是执行最小时间到达定时器的回调,另一个是处理poll队列中的事件。注意:Node的很多API都是基于事件订阅完成的,这些API的回调应该在poll阶段完成。当事件循环进入轮询阶段:当轮询队列不为空时,事件循环首先要遍历队列并同步执行回调,直到队列被清空或者回调执行次数达到系统上限。当轮询队列为空时,有两种情况。如果代码已经通过setImmediate()设置了回调,那么事件循环直接结束poll阶段,进入check阶段执行check队列中的回调。如果代码没有设置setImmediate()设置回调:如果设置了定时器,那么事件循环会在此时检查定时器,如果一个或多个定时器的下限时间已经到达,那么事件循环会回绕定时器阶段,并执行定时器的有效回调队列。如果不设置定时器,则事件循环阻塞在轮询阶段,等待此时事件回调加入轮询队列。Node的很多API都是基于事件订阅的,比如fs.readFile,这些回调应该在poll阶段完成。5.checksetImmediate()在此阶段执行。此阶段允许在轮询阶段结束后立即执行回调。如果poll阶段空闲,并且有setImmediate()设置的回调,那么事件循环直接跳转到检查执行,而不是阻塞在poll阶段等待poll事件的加入。注意:如果到达poll阶段,setImmediate()的优先级最高。只要轮询队列为空,注册了setImmediate(),无论是否有定时器到达下限时间,setImmediate()的代码都会先执行。6.close回调如果一个socket或者handle突然关闭(比如socket.destroy()),这个阶段会触发close事件,否则会被process.nextTick()触发。1.3请求对象对于Node中的异步I/O调用,回调函数是开发者不调用的。从JS发起调用到I/O操作完成,有一个中间产物叫做request对象。JS发起调用后,JS调用Node的核心模块,核心模块调用C++内置模块,内置模块通过libuv判断平台,进行系统调用。在进行系统调用时,将从JS层传入的方法和参数封装在一个request对象中,request对象放入线程池中执行。JS立即返回继续后续操作。1.4执行回调当线程可用时,线程会取出request对象进行I/O操作,执行完后将结果放入request对象,返回线程。在事件循环中,I/O观察者会不断地在线程池中寻找完成的请求对象,从中获取回调函数和数据,并执行。运行当前执行环境下可以运行的代码。每个事件消息都运行到完成,在此之前不会处理其他事件。这一点和C等一些语言不一样,它们可能在一个线程中,函数运行的时候突然停止了,然后其他线程又开始运行了。JS机制的一个典型缺点是,当一个事件处理时间过长时,后续的事件处理会延迟到事件处理结束。在浏览器环境下运行时,某个脚本运行时间过长,导致页面无响应。在Node环境下,可能会出现大量用户请求被挂起,无法及时响应的情况。2.非I/O异步API除了异步I/O外,还有一些与I/O无关的异步API,即:setTimeout()、setInterval()、process.nextTick()、setImmediate(),它们并不像普通的I/O操作那样真正需要等待事件的异步处理结束才回调,而是出于定时或延迟处理的原因而设计的。2.1setTimeout()和setInterval()的实现原理与异步I/O类似,只是不涉及I/O线程池。使用它们创建的定时器将被放入定时器队列中的红黑树中。每次执行事件循环,都会从对应的队列中取出,判断定时器是否超时。如果超过时间,将形成一个事件,立即执行回调。所以,就像在浏览器中一样,这是不精确的并且会被长同步事件阻塞。值得一提的是Node的setTimeout源码中://Nodesourcecodeafter*=1;//合并为数字或NaNif(!(after>=1&&after<=TIMEOUT_MAX)){if(after>TIMEOUT_MAX){process.emitWarning(...);}之后=1;//scheduleonnexttick,followsbrowserbehavior}表示如果此后未设置,或小于1,或大于TIMEOUT_MAX(2^31-1),将被强制设置为1ms。也就是说,setTimeout(xxx,0)其实等同于setTimeout(xxx,1)。2.2setImmediate()setImmediate()在校验阶段执行。它实际上是一个特殊的定时器,运行在事件循环的一个独立阶段。它使用libuv的API将回调设置为在轮询阶段结束后立即执行。我们来看这个例子:setTimeout(function(){console.log('setTimeout')},0)setImmediate(function(){console.log('setImmediate')})//输出不稳定setTimeoutandsetImmediatecomefirst队列之后,首先进入的是timers阶段。如果我们的机器性能一般,或者加入了一个同步的长耗时操作,那么进入timers阶段,已经过了1ms,那么会先执行setTimeout的回调。如果没有达到1ms,那么在timers阶段,超时时间还没有到,setTimeout回调还没有执行,eventloop已经到了poll阶段。这时候,队列是空的。这时候有些代码是setImmediate(),所以先执行setImmediate()。)回调函数,然后在下一个事件循环中执行setTimemout的回调函数。setTimeout(function(){console.log('settimeout')},0)setImmediate(function(){console.log('setImmediate')})for(leti=0;i<100000;i++){}//执行时间可以保证超过1ms//稳定输出:setTimeoutsetImmediate这样可以稳定输出。另一个例子:constfs=require('fs')fs.readFile('./filePath.js',(err,data)=>{setTimeout(()=>console.log('setTimeout'),0)setImmediate(()=>console.log('setImmediate'))console.log('started')for(leti=0;i<100000;i++){}})//outputstartedsetImmediatesetTimeoutherewe你会发现setImmediate总是在setTimeout之前执行。fs.readFile的回调在poll阶段执行。callback执行完后,setTimeout和setImmediate依次进入timer队列并检查,继续轮询,轮询队列为空。这时发现setImmediate,于是事件循环开始进入check阶段执行回调,然后在下一个事件循环的timers阶段执行setTimeout回调,虽然这个setTimeout已经到了超时时间。再举个栗子:同理,这段代码也是一样的:setTimeout(()=>{setImmediate(()=>console.log('setImmediate'));setTimeout(()=>console.log('setTimeout'),0);},0);上面代码在timers阶段执行完外部setTimeout回调后,将内部setTimeout和setImmediate入队,然后事件循环继续下一阶段,当到达poll阶段时,发现队列为空。这时候有一段代码是setImmediate(),所以直接进入check阶段执行response回调(注意没有检测timers队列中是否有超时事件,因为setImmediate()优先)。然后在下一个事件循环的timers阶段执行相应的回调。2.3process.nextTick()和Promise对于这两个,我们可以理解为一个microtask。也就是说,它们实际上并不是事件循环的一部分。有时我们希望立即异步执行一个任务,我们可能会使用延迟为0的定时器,但是这样的开销很大。我们可以改用process.nextTick(),它将传入的回调放到nextTickQueue队列中,等下一轮Tick后取出执行。不管eventloop走多远,都会在当前执行栈Call的运行结束,详见Nodejs官网。process.nextTick方法指定的回调函数总是在当前执行队列的末尾触发。多个process.nextTick语句总是一次执行(无论它们是否嵌套)。对process.nextTick的递归调用将是无止境的。主线程根本不会读取事件队列,导致后续调用一直阻塞,直到达到最大调用限制。相比定时器中使用红黑树的时间复杂度为0(lg(n)),process.nextTick()的时间复杂度为0(1),效率更高。举个复杂的栗子。如果你看懂了这个栗子,你基本上就可以全部看懂了:')process.nextTick(()=>{console.log('nextTick2')setImmediate(()=>console.log('setImmediate1'))process.nextTick(()=>console.log('nextTick3'))})setImmediate(()=>console.log('setImmediate2'))process.nextTick(()=>console.log('nextTick4'))console.log('sync2')setTimeout(()=>console.log('setTimout2'),0)},0)console.log('sync1')},0)//输出:sync1nextTick1setTimout1sync2nextTick2nextTick4nextTick3setImmediate2setImmediate1setTimout22.4结束过程。nextTick()效率最高,占用资源少,但会阻塞CPU的后续调用;setTimeout()不准确,可能会延迟执行,而且因为使用了红黑树,所以会消耗大量资源;setImmediate(),消耗资源少,不会造成阻塞,但效率也是最低的。网上的帖子大多是深浅不一,甚至有些不一致。以下文章是对学习过程的总结。如果发现错误请留言指出~参考:Node——AsynchronousI/ONodeExplorationEventCycleNode探索事件周期--setTimeout/setImmediate/process.nextTick的区别setTimeout/setImmediate/process的区别.nextTick详解setTimeout/setImmediate/process.nextTick的区别简单易懂setImmediate/nextTickNode.js探索:初识单线程Node.js|淘宝联储|一起努力吧~另外,你可以加入“前端下午茶交流群”微信群,长按识别下方二维码加我为好友,加群备注,我拉你入群群~
