nodejs的事件循环机制

一直以来,我写的的大部分JS代码都是在浏览器环境下运行,因此也了解过浏览器的事件循环机制,知道有macrotask和microtask的区别。但最近写node时,发现node的事件循环机制和浏览器端有很大的不同,特此深入地学习了下。

单线程

在传统web服务中,大多都是使用多线程机制来解决并发的问题,原因是I/O事件会阻塞线程,而阻塞就意味着要等待。而node的设计是采用了单线程的机制,但它为什么还能承载高并发的请求呢?因为node的单线程仅针对主线程来说,即每个node进程只有一个主线程来执行程序代码,但node采用了事件驱动的机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成,主线程本身只负责不断地调度,并没有执行真正的I/O操作。也就是说node实现的是异步非阻塞式。

事件循环机制

node能实现高并发的诀窍就在于事件循环机制,这个事件循环机制和浏览器端的相似但也有很多不同。根据node的官方介绍,node每次事件循环机制都包含了6个阶段:

  • timers阶段:这个阶段执行已经到期的timer(setTimeout、setInterval)回调
  • I/O callbacks阶段:执行I/O(例如文件、网络)的回调
  • idle, prepare 阶段:node内部使用
  • poll阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check阶段:执行setImmediate回调
  • close callbacks阶段:执行close事件回调,比如TCP断开连接

对于日常开发来说,我们比较关注的是timers、I/O callbacks、check阶段。node和浏览器相比一个明显的不同就是node在每个阶段结束后会去执行所有microtask任务。对于这个特点我们可以做个试验:

console.log('main');

setImmediate(function() {
    console.log('setImmediate');
});

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});

代码的执行结果是:

  1. main
  2. promise.then
  3. setImemediate

setImmediate 和 process.nextTick

相对于浏览器环境,node环境下多出了setImmediate和process.nextTick这两种异步操作。setImmediate的回调函数是被放在check阶段执行,即相当于事件循环的最后阶段了。而process.nextTick会被当做一种microtask,前面提到每个阶段结束后都会执行所有microtask任务,所以process.nextTick有种类似于插队的作用,可以赶在下个阶段前执行,但它和promise.then哪个先执行呢?通过一段代码来实验:

console.log('main');

process.nextTick(function() {
    console.log('nextTick')
})

new Promise(function(resolve, reject) {
    resolve();
}).then(function() {
    console.log('promise.then');
});

代码的执行结果是:

  1. main
  2. nextTick
  3. promise.then

事实证明,process.nextTick的优先级会比promise.then高。

process.nextTick的饥饿陷阱

process.nextTick的优势在于它能够插入到每个阶段之后,在当前阶段执行完毕后就能立马执行。然而它的这个优点也导致了如果调用不当就容易陷入饥饿陷阱。具体就是当递归地调用process.nextTick的时候,事件循环一直无法进入到下一个阶段,导致了后面阶段的事件一直无法被执行,产生饥饿问题。

看一个例子就很容易明白

let i = 0;
setImmediate(function() {
    console.log('setImmediate');
});
function callback() {
    console.log('nextTick' + i++);
    if (i < 1000) {
        process.nextTick(callback);
    }
}
callback();

执行的结果是
nextTick0
nextTick1
nextTick2

nextTick999
setImmediate

setImmediate的回调会一直等待到process.nextTick任务都完成后才能被执行。

小结

1.node的事件循环机制和浏览器的有所不同,多出了setImmediate 和 process.nextTick这两种异步方式。由于process.nextTick会导致I/O饥饿,所以官方也推荐使用setImmediate。
2.node虽然是单线程的设计,但它也能实现高并发。原因在于它的主线程事件循环机制和底层线程池的实现。
3.这种机制决定了node比较适合I/O密集型应用,而不适合CPU密集型应用。

写在最后

我个人开了一个公众号“前端搬运小工”,我会定期推送优秀的前端精选文章,拒绝无脑基础入门的文章,带给你不一样的前端视角。
nodejs的事件循环机制

上一篇:vue之nextTick


下一篇:从Vue.js源码看异步更新DOM策略及nextTick