介绍上一篇我们简单介绍了nodejs中事件事件和事件循环eventloop。本文将更进一步,继续讲解nodejs中的事件,并讨论setTimeout、setImmediate和process.nextTick的区别。nodejs中的事件循环虽然nodejs是单线程的,但是nodejs可以将操作委托给系统内核,由系统内核在后台处理这些任务。当任务完成后,通知nodejs,从而触发nodejs中的回调方法。这些回调将被添加到循环队列中并最终执行。通过这样的事件循环设计,nodejs最终可以实现非阻塞IO。nodejs中的事件循环分为阶段。下图列出了每个阶段的执行顺序:每个阶段维护一个回调队列,是一个FIFO队列。当进入一个阶段时,会先执行该阶段的任务,然后再执行属于该阶段的回调任务。当回调队列中的任务全部执行完毕或达到最大回调执行次数时,进入下一阶段。注意windows和linux的具体实现略有不同。这里我们只关注最重要的阶段。问题:在阶段执行过程中,为什么要限制回调执行的最大次数?答:在极端情况下,某个阶段可能需要执行大量的回调。如果执行这些回调的时间过长,会阻塞nodejs的运行,所以我们对回调的执行次数进行限制,避免nodejs长时间阻塞。阶段详解在上图中,我们列出了6个阶段,接下来我们将一一进行解释。TimerTimers的中文意思是定时器,意思是在给定的时间或时间间隔执行某个回调函数。通常有两种定时器函数:setTimeout和setInterval。一般来说,这些回调函数会在到期后尽可能执行,但会受到其他回调执行的影响。让我们看一个例子: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);//执行someAsyncOperation需要95毫秒才能完成someAsyncOperation(()=>{conststartCallback=Date.now();//执行需要10毫秒的操作...while(Date.now()-startCallback<10){//没做什么}});在上面的例子中,我们调用了someAsyncOperation,这个函数先回去执行readFile方法,假设这个方法耗时95ms。然后执行readFile的回调函数,这个回调会执行10ms。最后回去执行setTimeout中的回调。所以在上面的例子中,虽然setTimeout指定了在100ms后运行,但实际上是等了95+10=105ms才真正执行。pendingcallbacks阶段会执行一些系统回调操作。例如,当建立一个TCP连接时,TCP套接字接收到一个ECONNREFUSED信号。在某些liunx操作系统中会报这个错误,这个系统的回调会放在Runninginpendingcallbacks中。或者需要在下一个事件循环中执行的I/O回调操作。idle、prepareidle、prepare是内部使用的phase,这里就不介绍了。pollPoll会检测新的I/O事件并执行I/O相关的回调。注意,这里的回调指的是除关闭回调、定时器、setImmediate之外的几乎所有的回调事件。poll主要处理两件事:轮询I/O,计算block的时间,然后处理poll队列中的事件。如果poll队列不为空,事件循环会遍历队列中的回调,然后一个一个同步执行,直到队列被消费完或者达到回调个数限制。因为队列中的回调是一个一个同步执行的,所以可能会出现阻塞。如果轮询队列为空,如果代码中调用了setImmediate,会立即跳转到下一个check阶段,然后执行setImmediate中的回调。如果不调用setImmediate,它会继续等待新的回调加入队列并执行。check主要是执行setImmediate的回调。setImmediate可以看作是运行在单独阶段的唯一定时器,底层的libuvAPI用于计划回调。一般来说,如果在poll阶段有回调被setImmediate调用,当poll队列为空时poll阶段会立即结束,check阶段会执行相应的回调方法。关闭回调的最后一个阶段是处理关闭事件中的回调。比如某个socket突然关闭,会触发close事件,调用相关的回调。setTimeout和setImmediate的区别setTimeout和setImmediate有什么区别?从上图中的phase阶段可以看出,setTimeout中的回调是在timer阶段执行的,而setImmediate是在check阶段执行的。从语义上讲,setTimeout是指在给定时间后运行某个回调。而setImmediate是在当前循环执行完I/O操作后立即执行的。那么这两个方法的执行顺序有什么区别呢?下面我们举两个例子。在第一个示例中,两种方法都在主模块中运行:setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});这样,两个方法的执行顺序是不确定的,因为可能会受到其他执行程序的影响。第二个例子是在I/O模块中运行这两个方法:constfs=require('fs');fs.readFile(__filename,()=>{setTimeout(()=>{console.log('超时');},0);setImmediate(()=>{console.log('immediate');});});你会发现在I/O模块中,setImmediate必须在setTimeout之前执行。setTimeout和setImmediate都有一个返回值,我们可以用它来清除定时器:consttimeoutObj=setTimeout(()=>{console.log('timeoutbeyondtime');},1500);constimmediateObj=setImmediate(()=>{console.log('立即执行immediate');});constintervalObj=setInterval(()=>{console.log('interviewingtheinterval');},500);clearTimeout(timeoutObj);clearImmediate(immediateObj);clearInterval(intervalObj);清除操作也可以清除intervalObj。unref和refreshTimeout以及setInterval返回的对象是Timeout对象。如果这个超时对象是最后一个要执行的超时对象,可以使用unref方法取消它的执行。取消完成后,您可以使用ref恢复其执行。consttimerObj=setTimeout(()=>{console.log('我会跑吗?');});timerObj.unref();setImmediate(()=>{timerObj.ref();});注意,如果有多个超时对象,只有最后一个超时对象的unref方法会生效。process.nextTickprocess.nextTick也是一个异步API,但它不同于timer。如果我们在一个阶段调用process.nextTick,nextTick中的回调会在该阶段完成之前完成,进入事件循环的下一阶段。这样做会有一个问题,如果我们在process.nextTick中进行递归调用,这个阶段就会被阻塞,影响事件循环的正常执行。那么,为什么我们还有process.nextTick?考虑以下示例:letbar;functionsomeAsyncApiCall(callback){callback();}someAsyncApiCall(()=>{console.log('bar',bar);//undefined});酒吧=1;上面的例子中,我们定义了一个someAsyncApiCall方法,它执行传入的回调函数。这个回调函数要输出bar的值,但是bar的值是在someAsyncApiCall方法之后赋值的。这个例子最终会导致输出bar值未定义。我们的初衷是在用户程序执行后调用回调,那么我们可以使用process.nextTick改写上面的例子:>{console.log('bar',bar);//1});酒吧=1;再看一个实际的例子:constserver=net.createServer(()=>{}).listen(8080);server.on('listening',()=>{});上面的例子是最简单的nodejs创建web服务。上面的例子有什么问题?listen(8000)方法会立即绑定8000端口。但是此时,服务端的监听事件绑定代码还没有执行。这里其实用到了process.nextTick技术,这样无论我们在哪里绑定监听事件,都可以监听到listen事件。process.nextTick和setImmediate的区别在于process.nextTick是立即执行当前阶段的回调,而setImmediate是执行check阶段的回调。所以process.nextTick优先于setImmediate的执行顺序。事实上,process.nextTick和setImmediate的语义应该互换。因为process.nextTick意味着立即,而setImmediate意味着下一个报价。本文作者:flydean程序那些事儿本文链接:http://www.flydean.com/nodejs-event-more/本文来源:flydean的博客欢迎关注我的公众号:最“节目与活动”的通俗解读,最深度的干货,最简洁的教程,还有很多你不知道的小技巧等你来发现!
