Node.js事件循环

说到Node.js的事件循环网上已经有了不少形形色色的文章来说述其中的原理,说的大概都是一个意思,学习了一段时间,对Node.js事件循环有了必定的了解以后写一篇博客总结一下本身的学习成果。javascript

事件循环

在笔者看来事件与循环自己就是两个概念,事件是能够被控件识别的操做,如按下肯定按钮,选择某个单选按钮或者复选框。每一种控件有本身能够识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件。html

然而循环则是在GUI线程中包含有一个循环,然而这个循环对于开发者和用户来说是看不见的,只有关闭了程序以后该循环才会结束。当用户触发了一个按钮事件以后,就会产生响应的事件,这些时间被加入到一个队列中,用户在前台不断的产生事件,然然后台也在不断的处理这些时间,在处理的时候被加入到一个队列中,因为主循环中循环的存在会挨个处理这些对应的事件。java

而对于JavaScript来说的话因为JavaScript是单线程的,对于一个比较耗时的操做则是使用异步的方法解决(Ajax...)。对于不一样的异步事件来也是由不一样的线程各司其职来处理的。node

Node.js中的事件循环

Node.js的事件循环与浏览器的事件循环仍是有很大的区别的,当Node.js启动后,它会初始化事件轮询;处理已提供的输入脚本(或丢入REPL,本文不涉及到),它可能会调用一些异步的API函数调用,安排任务处理事件,或者调用process.nextTick(),而后开始处理事件循环。npm

有一点是很是明确的,事件循环一样运行在单线程环境下,JavaScript的事件循环是依靠于浏览器来实现的,然而Node.js则是依赖于Libuv来实现的。promise

根据Node.js官方介绍,每次事件循环都包含了6个阶段,对应到Libuv源码中的实现,以下图所示,图中显示了事件循环的概述以及执行顺序。浏览器

  1. timersj阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  2. I/O callbacks:执行一些系统调用错误,好比网络通讯的错误回调
  3. idle,prepare:仅node内部使用
  4. poll:获取新的I/O事件, 适当的条件下node将阻塞在这里
  5. check:执行 setImmediate() 的回调
  6. close callbacks:执行 socket 的 close 事件回调

下面是Node.js事件循环源代码:网络

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;
  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);
  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    // timers阶段
    uv__run_timers(loop);
    // I/O callbacks阶段
    ran_pending = uv__run_pending(loop);
    // idle阶段
    uv__run_idle(loop);
    // prepare阶段
    uv__run_prepare(loop);
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);
    // poll阶段
    uv__io_poll(loop, timeout);
    // check阶段
    uv__run_check(loop);
    // close callbacks阶段
    uv__run_closing_handles(loop);
    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;
  return r;
}

假设事件循环进入到某一个阶段,及时在这期间其余队列中的事件已经准备就绪,也会先将当前阶段对应队列中全部的回调方法执行完毕以后才会继续向下执行,结合代码也是可以很好的理解的。不难能够得出在事件循环系统中回调的执行顺序是有迹可循的,一样也会形成事件阻塞。异步

var fs = require("fs");
fs.readFile('input.txt', function (err, data) {
   if (err){
      console.log(err.stack);
      return;
   }
   console.log(data.toString());
});
fs.readFile('test.txt', function (err, data) {
   if (err){
      console.log(err.stack);
      return;
   }
   console.log(data.toString());
});
console.log("程序执行完毕");

对于整个事件循环有个一个大概的认知以后,接下来针对每一个阶段进行详细的说明。socket

timers

该阶段主要用来处理定时器相关的回调方法,当一个定时器超市后一个事件就会加入到该阶段的队列中,事件循环会跳转至这个阶段执行对应的回调方法。

定时器的回调会在触发后尽量早的被调用,为何要说尽量早的呢?由于实际的触发事件可能要比预先设置的时间要长。Node.js并不能保证timer在预设时间到了就会当即执行,由于Node.jstimer的过时检查不必定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。

I/O callbacks

在这个阶段中除了timers、setImmediate,以及close操做以外的大多数的回调方法都位于这个阶段执行。例一个TCP socket执行出现了一些错误,那么这个回调函数会在I/O callbacks阶段来执行。名字会让人误解为执行I/O回调处理程序,然而一些常见的回调则会再poll阶段进行处理。

I/O callbacks阶段主要通过以下过程:

  1. 检查是否有pending的I/O回调。若是有,执行回调。若是没有,退出该阶段。
  2. 检查是否有process.nextTick任务,若是有,所有执行。
  3. 检查是否有microtask,若是有,所有执行。
  4. 退出该阶段。

poll

对于Poll阶段其主要的功能主要有两点:

  1. 处理 poll 队列的事件
  2. 当有已超时的 timer,执行它的回调函数

当事件循环到达poll阶段时,若是这时没有要处理的定时器的回调方法,则会进行以下判断:

  1. 若是poll队列不为空,则事件循环会按照顺序便利执行队列中的回调方法,这个过程是同步的。
  2. 若是poll队列为空则会再次进行判断
    • 如有预设的setImmediate(),事件循环将结束poll阶段进入check阶段,并执行check阶段的任务队列
    • 若没有预设的setImmediate(),那么事件循环可能会进入等待状态,并等待新事件的产生,这也是该阶段为何被命名为poll的缘由。出了这些意外,该阶段还会不断的检查是否有相关的定时器超市,若是有就会跳转到timers阶段,而后执行对应的回调方法

check

该阶段执行setImmediate()的回调函数。关于setImmediate是一个比较特殊的定时器方法,setImmediate的回调则会加入到check队列中,从事件循环的阶段图能够知道,check阶段的执行顺序是在poll以后的。

通常状况下,事件循环到达poll阶段后,就会检查当前代码是否调用了setImmediate方法,这个在叙述poll阶段的时候已经有说起了,若是一个回调函数是被setImmediate方法调用的,事件循环则会跳出poll阶段从而进入到check阶段。(这一段有点重复...)

close

close阶段是用来管理关闭事件,用于清理应用程序的状态。如程序中的socket关闭等都会加入到close队列中,当本轮事件结束后则会进入下一轮循环。

小结

对于事件循环来讲每一个阶段都有一个任务队列,当事件循环到达某个阶段的时候,讲执行该阶段的任务队列,知道队列清空或执行的对调到达系统上限后,才会转入到下一个阶段。当全部的阶段被执行一次后,事件循环则就完成了一个tick

process.nextTick

这是Node.js特有的方法,它不存在于任何浏览器(以及进程对象)中,process.nextTick是一个异步的动做,而且让这个动做在事件循环中当前阶段执行完以后当即执行,也就是上面所说的tick

process.nextTick(() => {
    console.log("1")   
})
console.log("2")
//  2
//  1

官方对于process.nextTick有一段颇有意思的解释:从语义角度看,setImmediate(稍后会说到)应该比process.nextTick先执行才对,而事实相反,命名是历史缘由也很难再变。

然而对于process.nextTick来讲该方法并非事件循环中的一部分,可是它的回调方法确是由事件循环调用的,该方法定义的回调方法会被加入到nextTickQueue的队列中。相反地,nextTickQueue将会在当前操做完成以后当即被处理,而无论当前处于事件循环的哪一个阶段。

Node.jsprocess.nextTick进行了限制,若递归调用process.nextTick当倒带nextTickQueue最大限制以后则会抛出一个错误。

function nextTick (i){
    while(i<9999){
        process.nextTick(nextTick(i++));
    }
}

//  Maxmum call stack size exceeded
nextTick(0);

既然说process.nextTick也是存在于队列中,那么其执行顺序也是根据程序所编写顺序执行的。

process.nextTick(() => {
    console.log(1)
});
process.nextTick(() => {
    console.log(2)
});

//  1
//  2

和其它回调函数同样,process.nextTick定义的回调也是由事件循环执行的,若是process.nextTick的回调方法中出现了阻塞操做,后面的要执行的回调函数一样会被阻塞。process.nextTick会在各个事件阶段之间执行,一旦执行,要直到nextTickQueue被清空,才会进入到下一个事件阶段,因此若是递归调用process.nextTick,会致使出现I/O starving的问题,好比下面例子的readFile已经完成,但它的回调一直没法执行。

const fs = require('fs')
const starttime = Date.now()
let endtime;
fs.readFile('text.txt', () => {
  endtime = Date.now()
  console.log('finish reading time: ', endtime - starttime)
})
let index = 0
function handler () {
  if (index++ >= 1000) return
  console.log(`nextTick ${index}`)
  process.nextTick(handler)
}
handler();

//  nextTick 1
//  nextTick 2
//  ......
//  nextTick 999
//  nextTick 1000
//  finish reading time: 170

process.nextTick() vs setImmediate()

seImmediate方法不属于ECMAScript标准,而是Node.js提出的新方法,它一样将一个回调函数加入到事件队列中,不一样于setTimeoutsetIntervalsetImmediate并不接受一个时间做为参数,setImmediate的事件会在当前事件循环的结尾触发,对应的回调方法会在当前事件循环的末尾(check)执行。虽然它确实存在于某些浏览器中,但并未在全部浏览器中达到一致的行为,所以在浏览器中使用时,您须要很是当心。它相似于setTimeout(fn,0)代码,但有时会优先于它。这里的命名也不是最好的。

  1. process.nextTick中的回调在事件循环的当前阶段中被当即执行。
  2. setImmediate中的回调在事件循环的下一次迭代或tick中被执行

本质上,它们两个的名字应该互相调换一下。process.nextTick()的执行时机比setImmediate()要更及时(上面有提过)。实施这项改变将致使不少npm包没法使用。天天都有不少新模块被加入,这意味着每等待一天,就会有更多潜在的破坏发生。虽然他们的名字相互混淆,但将它们调换名字这种事是不会发生的(建议开发者在全部地方使用setImmediate,这样程序更容易让人理解)。

仍然使用上述例子,若把nextTick替换成setImmediate会怎样呢?

const fs = require('fs')
const starttime = Date.now()
let endtime;
fs.readFile('text.txt', () => {
  endtime = Date.now()
  console.log('finish reading time: ', endtime - starttime)
})
let index = 0
function handler () {
  if (index++ >= 1000) return
  console.log(`setImmediate ${index}`)
  setImmediate(handler)
}
handler();

// setImmediate 1
// setImmediate 2
// finish reading time: 80
// ......
// setImmediate 999
// setImmediate 1000

这是由于嵌套调用的setImmediate()回调,被排到了下一次事件循环才执行,因此不会出现阻塞。

setImmediate vs setTimeout

定时器在Node.js和浏览器中的表现形式是相同的。关于定时器的一个重要的事情是,咱们提供的延迟不表明在这个时间以后回调就会被执行。它的真正含义是,一旦主线程完成全部操做(包括微任务)而且没有其它具备更高优先级的定时器,Node.js将在此时间以后执行回调。

  1. setImmediate()被设计在poll阶段结束后当即执行回调
  2. setTimeout()被设计在指定下限时间到达后执行回调
setTimeout(function timeout () {
  console.log('timeout');
},0);

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

//  结果一
//  timeout
//  immediate
/**--------华丽的分割线--------**/
//  结果二
//  immediate
//  timeout

why?为何会有两个结果,笔者在研究这里的时候也是有些不太明白,因而又作了第二个例子:

var fs = require('fs')
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
});
//  运行N次
//  immediate
//  timeout
  1. 若是二者都在主模块调用,那么执行前后取决于进程性能,即随机。
  2. 若是二者都不在主模块调用,那么setImmediate的回调永远先执行。

虽然结论得出来了,可是这又是为啥呢?回想一下文章上半段所叙述的事件循环。首先进入timer阶段,若是咱们的机器性能通常,那么进入timer阶段时,1毫秒可能已通过去了(setTimeout(fn,0)等价于setTimeout(fn,1)),那么setTimeout的回调会首先执行。若是没到一毫秒,那么咱们能够知道,在check阶段,setImmediate的回调会先执行。为何fs.readFile回调里设置的,setImmediate始终先执行?由于fs.readFile的回调执行是在poll阶段,因此,接下来的check阶段会先执行setImmediate的回调。咱们能够注意到,UV_RUN_ONCE模式下,事件循环会在开始和结束都去执行timer

练习题

阅读完本文章有什么收获呢?不如看下下面的代码,预测一下输出结果是什么样的。先不要急着看答案额...

const fs = require('fs');
console.log('beginning of the program');
const promise = new Promise(resolve => {
    console.log('I am in the promise function!');
    resolve('resolved message');
});
promise.then(() => {
    console.log('I am in the first resolved promise');
}).then(() => {
    console.log('I am in the second resolved promise');
});
process.nextTick(() => {
    console.log('I am in the process next tick now');
});
fs.readFile('index.html', () => {
    console.log('==================');
    setTimeout(() => {
        console.log('I am in the callback from setTimeout with 0ms delay');
    }, 0);
    setImmediate(() => {
        console.log('I am from setImmediate callback');
    });
});
setTimeout(() => {
    console.log('I am in the callback from setTimeout with 0ms delay');
}, 0);
setImmediate(() => {
    console.log('I am from setImmediate callback');
});

// beginning of the program
// I am in the promise function!
// I am in the process next tick now
// I am in the first resolved promise
// I am in the second resolved promise
// I am in the callback from setTimeout with 0ms delay
// I am from setImmediate callback
// ==================
// I am from setImmediate callback
// I am in the callback from setTimeout with 0ms delay

总结

对于本文中一些知识点任然有些模糊,懵懵懂懂,一直都在学习中,经过学习事件循环也看了一些文献,在其中看到了这一句话:除了你的代码,一切都是同步的,我以为颇有道理,对于理解事件循环颇有帮助。

  1. Node.js的事件循环分为6个阶段
  2. process.nextTick不属于事件循环,可是产生的回调会加入到nextTickQueue
  3. setImmediatesetTimeout的执行顺序会受到环境所影响

文章略长若文章中有哪些错误,请在评论区指出,我会尽快作出修正。你们能够踊跃发言共同进步,交流。