深深分析Node

时间:2020-01-31 16:49来源:亚洲城ca88唯一官方网站
时间: 2019-03-26阅读: 301标签: node Node.js Event Loop 的理解 Timers,process.nextTick() Event Loop阶段描述图 发布于 1 年前 作者vincentLiuxiang8167 次浏览 最后一次编辑是 10 个月前 来自 分享 timers 写n

时间: 2019-03-26阅读: 301标签: node

Node.js Event Loop 的理解 Timers,process.nextTick()

Event Loop阶段描述图

发布于 1 年前 作者 vincentLiuxiang 8167 次浏览 最后一次编辑是 10 个月前 来自 分享

timers

写node.js有一段时间了,一直在理解event loop这个概念,国内外的文章翻阅了也不少,但是对event loop能讲解清楚的还是不多。 最后还是查阅的nodejs对event loop的官方解释才恍然大悟,如获至宝。

timer阶段处理setTimeout于setInterval回调,开始处理的时机与poll阶段有关联。

写这篇文章的目的是将自己对该文章的理解做一个记录,官方文档链接The Node.js Event Loop, Timers, and process.nextTick()

pending callbacks

文章内容可能有错误理解的地方,希望能和大家探讨一下,欢迎批评指正!

该阶段执行某些系统操作的回调,比如TCP套接字在连接时收到ECONNREFUSED。

Node.js Event Loop 的理解 Timers,process.nextTick()

网上有一些将该阶段称为I/O callbacks的文章都是过时错误的,具体可以移步Node.js官方库下面的这个issue:。

Event Loop的解释

英文原文: When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时, event loop会转入下一下阶段.

idle, prepare

注意上面六个阶段都不包括 process.nextTick()

内部使用,忽略。

poll阶段

poll阶段是衔接整个event loop各个阶段比较重要的阶段,为了便于后续例子的理解,本文和原文的介绍顺序不一样,本文先讲这个阶段;

在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。

poll 阶段有两个主要的功能:

  1. 处理poll队列(poll quenue)的事件(callback);
  2. 执行timers的callback,当到达timers指定的时间时;

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
  • 如果poll queue为空,将会发生下面情况:
    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;

如果event loop进入了 poll阶段,且代码设定了timer:

  • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子

  • 注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异。

poll

example 1

var fs = require('fs');

function someAsyncOperation (callback) {
  // 花费2毫秒
  fs.readFile(__dirname   '/'   __filename, callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;

setTimeout(function () {
  var delay = Date.now() - timeoutScheduled;
  console.log('setTimeout: '   (delay)   "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
});

结果: 先执行someAsyncOperation的callback,再执行setTimeout callback

-> node eventloop.js
setTimeout: 22ms have passed since I was scheduled
fileReaderTime 2

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。

poll是一个核心阶段,等新I/O事件的触发,以及执行I/O相关回调。Node.js中出现异步的绝大部分情况都是I/O操作,它们的回调基本都在这个阶段被执行。

example 2

var fs = require('fs');

function someAsyncOperation (callback) {
  var time = Date.now();
  // 花费9毫秒
  fs.readFile('/path/to/xxxx.pdf', callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;
var delay = 0;

setTimeout(function () {
  delay = Date.now() - timeoutScheduled;
}, 5);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
  console.log('setTimeout: '   (delay)   "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
});

结果:setTimeout callback先执行,someAsyncOperation callback后执行

-> node eventloop.js
setTimeout: 7ms have passed since I was scheduled
fileReaderTime 9

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行5ms时,poll依然空闲,但已设定timer,且时间已到达,因此,event loop需要循环到timer阶段,执行setTimeout callback,由于从poll --> timer中间要经历check,close阶段,这些阶段也会消耗一定时间,因此执行setTimeout callback实际是7毫秒 然后又回到poll阶段等待异步i/o完成,在9毫秒时fs.readFile完成,其callback加入poll queue并执行。

poll阶段主要做两件事:

setTimeout 和 setImmediate

二者非常相似,但是二者区别取决于他们什么时候被调用.

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () {
  console.log('timeout');
},0);

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

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

计算需要为新的的I/O事件等待多久

关于这点的原因,笔者目前还未弄清楚。。。其原因读者可去看@hyj1991 的留言,讲解非常清晰

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

$ node timeout_vs_immediate.js
immediate
timeout

理解了event loop的各阶段顺序这个例子很好理解: 因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

当进入poll阶段,如果队列为空且不存在setImmediate与就绪的timer,Node.js会在这里block一定的时间等待新的I/O事件到来,然后立即执行其回调。这种情况具体block等待多久是不具体的,但如果在block一定时间后仍没有新到达的I/O事件,可以肯定循环依旧会进入check阶段或者回到timer阶段。

process.nextTick()

千呼万唤始出来,终于到了讲process.nextTick()的时候,来来来喝口水休息休息。

注意:本文讲解的process.nextTick是基于v0.10及以上版本

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});

-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout

从poll —> check阶段,先执行process.nextTick, nextTick1 nextTick2 然后进入check,setImmediate, setImmediate 执行完setImmediate后,出check,进入close callback前,执行process.nextTick nextTick3 最后进入timer执行setTimeout setTimeout

process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。

处理该阶段队列中的事件

example process.nextTick()

当递归调用process.nextTick时,即使fs.readFile完成,其callback无机会执行

var fs = require('fs');
var starttime = Date.now();
var endtime = 0;

fs.readFile(__filename, () => {
  endtime = Date.now();
  console.log('finish reading time:',endtime - starttime);
});

var index = 0;

function nextTick () {
  if (index > 1000) return;
  index  ;
  console.log('nextTick');
  process.nextTick(nextTick);
}

nextTick();

-> node eventloop.js
nextTick
nextTick
...
nextTick
nextTick
finish reading time: 246

当进入poll阶段,如果队列不为空且没有就绪的timer,Node.js会在这里执行队列中的callback直到队列为空或者执行的callback数达到系统设定的某个值。随后Node.js检查是否存在预设的setImmediate,存在话就进入check阶段,否则开始检查timer就绪情况选择回到timer阶段或者进入check阶段。

example setImmediate

将process.nextTick替换成setImmediate后,由于setImmediate只在check阶段执行,那么所有的callback都有机会执行。

var fs = require('fs');

fs.readFile(__filename, () => {
  console.log('finish reading');
});

var index = 0;

function Immediate () {
  if (index > 100) return;
  index  ;
  console.log('setImmediate');
  setImmediate(Immediate);
}

Immediate()

-> node eventloop.js
setImmediate
setImmediate
setImmediate
setImmediate
finish reading time: 19
...
setImmediate
setImmediate

https://cnodejs.org/topic/57d68794cb6f605d360105bf

https://github.com/nodejs/node/blob/v6.x/doc/topics/event-loop-timers-and-nexttick.md

对于poll阶段,通过阅读官方的文档有些细节也没弄清楚,用伪代码表示出来:

enter pool phase:if (has timer scheduled) { // 官方没有提到这种情况会做什么}else { if (isEmpty(queue)) { if (has(setImmediate)) { // 进入check阶段 } else if (!isEmpty(timer)) { // 回到timer阶段 } else { // 等待新的I/O事件 // 新的I/O事件触发回调立即执行,执行完成之后的逻辑不清楚 } // 目前看来只有存在setImmediate时才会进入check阶段,这肯定不合理 }//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力 if (!isEmpty(queue)) { let result = execute(queue); if (result === 'queue is empty') { // 官方没讲后续逻辑 // 猜测是回到队列为空的处理逻辑中 } if (result === 'reached hard limit') { // 官方没有解释这里的后续逻辑 // 也许与queue is empty一样对待 } }}//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

疑惑重点是从poll阶段出来的时机以及去向不是非常明确,但以我目前的水平和精力只能到此为止。

check

当poll阶段执行完成会进入到check阶段执行,该阶段的执行内容是所有setImmediate回调。

close callbacks

socket的异常关闭,'close'事件的回调会在该阶段执行。

process.nextTick

process.nextTick经常被用来做异步调用,但它并不属于事件循环的内容,process.nextTick中的回调被放在nextTickQueue中等待“当前操作”完成后被立即处理,与事件循环中的阶段没有联系,当前操作的原文定义是:“An operation is defined as a transition from the underlying C/C handler, and handling the JavaScript that needs to be executed.”,指的是在一段Javascript代码执行完切换到C/C 层时会处理nextTickQueue。

在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

文章提到了一个特例是Deduplication,这是Node.js内部一个优化特性,当在timer和check阶段,同时有多个需要执行的回调时,切换只会发生一次,所以nextTick回调执行在这种情况下看似有所延后。 代码示例:

setImmediate(() = { console.log('1'); process.nextTick(() = console.log('2'));});setImmediate(() = { console.log('3'); process.nextTick(() = console.log('4'));});

存在两个setImmediate,进入check阶段后需要在执行所有setImmediate的回调代码后才会产生切换,从而执行nextTick回调,因此上面代码的运行结果是:“1 3 2 4”,除上述场景外,nextTick都会先于setImmediate执行。

总结

因为Node.js的Event Loop我看了有那么2、3回,但经常忘,所以这次记录下来,做个备忘。由于太多知识容易忘记,又发现写文章的一个优点:“帮助记忆便于复习”。

编辑:亚洲城ca88唯一官方网站 本文来源:深深分析Node

关键词: 亚洲城ca88