主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。下面本篇文章就来带大家掌握Node.js中的eventloop,希望对大家有所帮助!
其实在前面的文章我也讲述过浏览器中的eventloop。然而在NodeJs中的eventloop与浏览器的是有区别的。对于写nodejs的人来说掌握eventloop是一项很重要的技能。因为这意味着你不仅是会写js,而对NodeJs也是有研究的。
为什么要有eventloop?
我们知道NodeJs的本质是把浏览器的v8搬到了操作系统中运行,因此也把浏览器的事件循环拿过来了。可是为什么会出现eventloop这样的设计呢?
从历史原因上来看,js在设计时只是一门很简单的为了在页面上操作一下dom的语言(相信大家都听过js只用了10天就设计出来的故事)。出于这个目标,我们当然希望js的运行尽可能的简单,轻量,有多轻呢?轻到js的渲染引擎是在一个线程中运行的。
那么问题来了如果是在一个线程上运行js,当代码是线性的时候,当然是没有问题的。但在页面上,我们需要用户的交互,而这些交互是不知道为什么时候发生的。那js要怎么处理?如果眼前有正在运行的代码,一个用户交互进来之后,程序该怎么反应?如果先处理用户的交互,那原来的程序就会被暂停(也就是阻塞)。为了避免这种阻塞,js采用了一个办法,就是用一个消息队列,来存放这种用户交互。等所有的程序跑完之后,再去消息队列中拿交互事件,然后执行。这样就解决了阻塞的问题了。
浏览器的eventloop
我们都知道浏览器在浏览页面的时候,用户交互是随时可能发生的,为了可以即时响应用户。js是不会关闭的,他会不停的循环。大致如下:
向消息队列拿任务-->执行任务-->执行完毕--> 向消息队列拿任务--> ....
当然我们在之前的事件循环文章中讲过,为了给不同的异步任务分类,在事件循环中其实是有宏任务和微任务的区分的。他们的执行大致为
向消息队列拿微任务-->执行微任务-->微任务执行完毕--> 向消息队列拿宏任务-->执行宏任务-->宏任务执行完毕-->向消息队列拿微任务-->...
NodeJs的eventloop
node的事件循环其实大致思路跟在浏览器上的是相似的,但nodeJs对不同的宏任务又作出了不同时期的区分。下面是官方的流程图:
可以看到nodeJs中每次事件循环被分成了具体的6个时期,每个时期会用指定的宏任务。然后在每个时期的宏任务执行之前,会优先执行完微任务队列。
总览
timers | 执行由setTimeout() 和 setInterval() 触发的回调 |
---|---|
pending callbacks | 执行延迟到下一个循环迭代的I / O回调 |
idle, prepare | 只在内部使用,开发者可以不关注 |
poll | 检索新的I / O事件;执行I / O相关的回调(会执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段) |
check | 执行setImmediate() |
close callbacks | 比如socket.on('close', …) |
其实通过上述表格,我们已经很清晰知道整个事件循环机制的执行顺序了。但可能大家还会有一些疑问。下面来详细讲一下。
pending callbacks
这个阶段其实是处理由于操作系统出错,导致一些本应在上次事件循环中执行的回调。例如一些TCP错误。因此这部分,开发者不能主动操作,是NodeJs的一些容错机制。
check
同样的,setImmediate是nodejs特有的api,他可以立即创建一个异步宏任务。不仅如此,nodejs在事件循环中还专门设了一个check时期,在这个时期会专门执行setImmediate的回调。甚至你可以在这个时期中如果不停的产生setImmediate回调,eventloop会优先处理。
close callbacks
这个时期处理关闭事件,如socket.on('close', …) 等这样可以确保在一些通讯结束前,所有任务都完成了。
微任务在eventloop中
我们先来回顾浏览器与nodejs的差异:
宏任务:
任务 | 浏览器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务:
任务 | 浏览器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
可以看到process.nextTick是nodejs特有的微任务,不仅如此,process.nextTick()的优先级高于所有的微任务,每一次清空微任务列表的时候,都是先执行 process.nextTick()
执行差异
不仅是任务类型上有差异,在执行上2个环境其实也有差异。在浏览器上执行任务的时候,每执行一个宏任务之前,需要先确保微任务队列执行完了。而在nodejs上是每个时期之前,先确保微任务队列执行完。也就是说在假如在timer时期,会先把所有setTimeout,setInterval的宏任务执行完。在执行完微任务,再进入下个时期。
注意:以上执行规则是在nodejs的v11版本之前的规则。在11版本之后nodejs的执行输出是跟浏览器一样的。
setImmediate() vs setTimeout()
setImmediate() 和 setTimeout()的执行先后顺序是不一定的,就是说如果你不停地执行以下代码,每次得到的结果可能是不一样的。
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
其中的原因是程序对时间的处理是有误差的。在setTimeout方法中设置的时间,不一定是准确的。同时在回调触发时,也无法确认事件循环处在哪个时期,可能是timer,也可能是check。所有会有不同的结果。
总结
eventloop是js运行机制里的重点内容,对于NodeJs来说,eventloop的操作空间则更大。因为它被细分为不同的时期,从而让我们可能把逻辑进一步细化。同时利用nextTick的最高优先级,可以写出在浏览器无法实现的代码。因此对于深入NodeJs的开发者来说,eventloop往往是他们考察新人对NodeJs理解的第一步。