关于node.js:深度理解NodeJS事件循环

导读

ALL THE TIME,咱们写的的大部分javascript代码都是在浏览器环境下编译运行的,因而可能咱们对浏览器的事件循环机制理解比Node.JS的事件循环更深刻一些,然而最近写开始深刻NodeJS学习的时候,发现NodeJS的事件循环机制和浏览器端有很大的区别,特此记录来深刻的学习了下,以帮忙本人及小伙伴们遗记后查阅及了解。

什么是事件循环

首先咱们须要理解一下最根底的一些货色,比方这个事件循环,事件循环是指Node.js执行非阻塞I/O操作,只管==JavaScript是单线程的==,但因为大多数==内核都是多线程==的,Node.js会尽可能将操作装载到零碎内核。因而它们能够解决在后盾执行的多个操作。当其中一个操作实现时,内核会通知Node.js,以便Node.js能够将相应的回调增加到轮询队列中以最终执行。

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

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • [x] 1. timers 阶段: 这个阶段执行 setTimeout(callback)setInterval(callback) 预约的 callback;
  • [x] 2. I/O callbacks 阶段: 此阶段执行某些零碎操作的回调,例如TCP谬误的类型。 例如,如果TCP套接字在尝试连贯时收到 ECONNREFUSED,则某些* nix零碎心愿期待报告谬误。 这将操作将期待在==I/O回调阶段==执行;
  • [x] 3. idle, prepare 阶段: 仅node外部应用;
  • [x] 4. poll 阶段: 获取新的I/O事件, 例如操作读取文件等等,适当的条件下node将阻塞在这里;
  • [x] 5. check 阶段: 执行 setImmediate() 设定的callbacks;
  • [x] 6. close callbacks 阶段: 比方 socket.on(‘close’, callback) 的callback会在这个阶段执行;

事件循环详解

这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,别离是 应用层V8引擎层Node API层LIBUV层

  • 应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比方 http,fs
  • V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和上层 API 交互
  • NodeAPI层: 为下层模块提供零碎调用,个别是由 C 语言来实现,和操作系统进行交互 。
  • LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的外围 。

每个循环阶段内容详解

timers阶段 一个timer指定一个上限工夫而不是精确工夫,在达到这个上限工夫后执行回调。在指定工夫过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会提早它们。

  • 留神:技术上来说,poll 阶段管制 timers 什么时候执行。
  • 留神:这个上限工夫有个范畴:[1, 2147483647],如果设定的工夫不在这个范畴,将被设置为1。

I/O callbacks阶段 这个阶段执行一些零碎操作的回调。比方TCP谬误,如一个TCP socket在想要连贯时收到ECONNREFUSED,
类unix零碎会期待以报告谬误,这就会放到 I/O callbacks 阶段的队列执行.
名字会让人误会为执行I/O回调处理程序, 实际上I/O回调会由poll阶段解决.

poll阶段 poll 阶段有两个次要性能:(1)执行上限工夫曾经达到的timers的回调,(2)而后解决 poll 队列里的事件。
当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会产生上面两件事之一:

  • 如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数达到零碎下限;
  • 如果 poll 队列为空,则产生以下两件事之一:

    • 如果代码曾经被setImmediate()设定了回调, event loop将完结 poll 阶段进入 check 阶段来执行 check 队列(外面的回调 callback)。
    • 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段期待回调被退出 poll 队列,并立刻执行。
  • 然而,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段闲暇状态):
    event loop将查看timers,如果有1个或多个timers的上限工夫曾经达到,event loop将绕回 timers 阶段,并执行 timer 队列。

check阶段 这个阶段容许在 poll 阶段完结后立刻执行回调。如果 poll 阶段闲暇,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是持续期待。

  • setImmediate() 实际上是一个非凡的timer,跑在event loop中一个独立的阶段。它应用libuv的API
    来设定在 poll 阶段完结后立刻执行回调。
  • 通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段期待 incoming connection, request 等等。然而,只有有被setImmediate()设定了回调,一旦 poll 阶段闲暇,那么程序将完结 poll 阶段并进入 check 阶段,而不是持续期待 poll 事件们 (poll events)。

close callbacks 阶段 如果一个 socket 或 handle 被忽然关掉(比方 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发

这里呢,咱们通过伪代码来阐明一下,这个流程:

// 事件循环自身相当于一个死循环,当代码开始执行的时候,事件循环就曾经启动了
// 而后顺序调用不同阶段的办法
while(true){
// timer阶段
    timer()
// I/O callbacks阶段
    IO()
// idle阶段
    IDLE()
// poll阶段
    poll()
// check阶段
    check()
// close阶段
    close()
}
// 在一次循环中,当事件循环进入到某一阶段,退出进入到check阶段,忽然timer阶段的事件就绪,也会等到以后这次循环完结,再去执行对应的timer阶段的回调函数 
// 上面看这里例子
const fs = require('fs')

// timers阶段
const startTime = Date.now();
setTimeout(() => {
    const endTime = Date.now()
    console.log(`timers: ${endTime - startTime}`)
}, 1000)

// poll阶段(期待新的事件呈现)
const readFileStart =  Date.now();
fs.readFile('./Demo.txt', (err, data) => {
    if (err) throw err
    let endTime = Date.now()
    // 获取文件读取的工夫
    console.log(`read time: ${endTime - readFileStart}`)
    // 通过while循环将fs回调强制阻塞5000s
    while(endTime - readFileStart < 5000){
        endTime = Date.now()
    }

})


// check阶段
setImmediate(() => {
    console.log('check阶段')
})
/*控制台打印check阶段read time: 9timers: 5008通过上述后果进行剖析,1.代码执行到定时器setTimeOut,目前timers阶段对应的事件列表为空,在1000s后才会放入事件2.事件循环进入到poll阶段,开始一直的轮询监听事件3.fs模块异步执行,依据文件大小,可能执行工夫长短不同,这里我应用的小文件,事件大略在9s左右4.setImmediate执行,poll阶段临时未监测到事件,发现有setImmediate函数,跳转到check阶段执行check阶段事件(打印check阶段),第一次工夫循环完结,开始下一轮事件循环5.因为工夫仍未到定时器截止工夫,所以事件循环有一次进入到poll阶段,进行轮询6.读取文件结束,fs产生了一个事件进入到poll阶段的事件队列,此时事件队列筹备执行callback,所以会打印(read time: 9),人工阻塞了5s,尽管此时timer定时器事件曾经被增加,然而因为这一阶段的事件循环为实现,所以不会被执行,(如果这里是死循环,那么定时器代码永远无奈执行)7.fs回调阻塞5s后,以后事件循环完结,进入到下一轮事件循环,发现timer事件队列有事件,所以开始执行 打印timers: 5008ps:1.将定时器延迟时间改为5ms的时候,小于文件读取工夫,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行结束持续进行事件循环check阶段timers: 6read time: 50082.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段曾经有callback,那么会间接执行,而后执行结束在下一阶段循环,执行check阶段,poll队列的回调函数timers: 2check阶段read time: 7 */

走进案例解析

咱们来看一个简略的EventLoop的例子:

参考nodejs进阶视频解说:进入学习

const fs = require('fs');
let counts = 0;

// 定义一个 wait 办法
function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

// 读取本地文件 操作IO
function asyncOperation (callback) {
  fs.readFile(__dirname + '/' + __filename, callback);
}

const lastTime = Date.now();

// setTimeout
setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

// process.nextTick
process.nextTick(() => {
  // 进入event loop
  // timers阶段之前执行
  wait(20);
  asyncOperation(() => {
    console.log('poll');
  });  
});

/** * timers 21ms * poll */

这里呢,为了让这个setTimeout优先于fs.readFile 回调, 执行了process.nextTick, 示意在进入timers阶段前, 期待20ms后执行文件读取.

1. nextTicksetImmediate

  • process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行完结, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.
  • setImmediate 的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行,

nextTick 递归的危害

因为nextTick具备插队的机制,nextTick的递归会让事件循环机制无奈进入下一个阶段. 导致I/O解决实现或者定时工作超时后依然无奈执行, 导致了其它事件处理程序处于饥饿状态. 为了避免递归产生的问题, Node.js 提供了一个 process.maxTickDepth (默认 1000)。

const fs = require('fs');
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function nextTick () {
  process.nextTick(() => {
    wait(20);
    console.log('nextTick');
    nextTick();
  });
}

const lastTime = Date.now();

setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

nextTick();

此时永远无奈跳到timer阶段去执行setTimeout外面的回调办法, 因为在进入timers阶段前有一直的nextTick插入执行. 除非执行了1000次到了执行下限,所以下面这个案例会一直地打印出nextTick字符串

2. setImmediate

如果在一个I/O周期内进行调度,setImmediate() 将始终在任何定时器(setTimeout、setInterval)之前执行.

3. setTimeoutsetImmediate

  • setImmediate()被设计在 poll 阶段完结后立刻执行回调;
  • setTimeout()被设计在指定上限工夫达到后执行回调;

无 I/O 解决状况下:

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

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

执行后果:

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

从后果,咱们能够发现,这里打印输出进去的后果,并没有什么固定的先后顺序,偏差于随机,为什么会产生这样的状况呢?

答:首先进入的是timers阶段,如果咱们的机器性能个别,那么进入timers阶段,1ms曾经过来了 ==(setTimeout(fn, 0)等价于setTimeout(fn, 1))==,那么setTimeout的回调会首先执行。

如果没有到1ms,那么在timers阶段的时候,上限工夫没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,于是往下持续,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

问题总结:而咱们在==执行启动代码==的时候,进入timers的时间延迟其实是==随机的==,并不是确定的,所以会呈现两个函数执行程序随机的状况。

那咱们再来看一段代码:

var fs = require('fs')

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

打印后果如下:

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

# ... 省略 n 屡次应用 node test.js 命令 ,后果都输入 immediate timeout

这里,为啥和下面的随机timer不统一呢,咱们来剖析下起因:

起因如下:fs.readFile的回调是在poll阶段执行的,当其回调执行结束之后,poll队列为空,而setTimeout入了timers的队列,此时有代码 setImmediate(),于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行回调。

当然,上面的小案例同理:

setTimeout(() => {
    setImmediate(() => {
        console.log('setImmediate');
    });
    setTimeout(() => {
        console.log('setTimeout');
    }, 0);
}, 0);

以上的代码在timers阶段执行内部的setTimeout回调后,内层的setTimeoutsetImmediate入队,之后事件循环持续往后面的阶段走,走到poll阶段的时候发现队列为空,此时有代码有setImmedate(),所以间接进入check阶段执行响应回调(==留神这里没有去检测timers队列中是否有成员达到上限事件,因为setImmediate()优先==)。之后在第二个事件循环的timers阶段中再去执行相应的回调。

综上所演示,咱们能够总结如下:

  • 如果两者都在主模块中调用,那么执行先后取决于过程性能,也就是你的电脑好撇,当然也就是随机。
  • 如果两者都不在主模块调用(被一个异步操作包裹),那么setImmediate的回调永远先执行

4. nextTickPromise

概念:对于这两个,咱们能够把它们了解成一个微工作。也就是说,它其实不属于事件循环的一部分
那么他们是在什么时候执行呢?
不论在什么中央调用,他们都会在其所处的事件循环最初,事件循环进入下一个循环的阶段前执行。

setTimeout(() => {
    console.log('timeout0');
    new Promise((resolve, reject) => { resolve('resolved') }).then(res => console.log(res));
    new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('timeout resolved')
      })
    }).then(res => console.log(res));
    process.nextTick(() => {
        console.log('nextTick1');
        process.nextTick(() => {
            console.log('nextTick2');
        });
    });
    process.nextTick(() => {
        console.log('nextTick3');
    });
    console.log('sync');
    setTimeout(() => {
        console.log('timeout2');
    }, 0);
}, 0);

控制台打印如下:

C:\Users\92809\Desktop\node_test>node test.js
timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout2
timeout resolved

最总结:timers阶段执行外层setTimeout的回调,遇到同步代码先执行,也就有timeout0sync的输入。遇到process.nextTickPromise后入微工作队列,顺次nextTick1nextTick3nextTick2resolved入队后出队输入。之后,在下一个事件循环的timers阶段,执行setTimeout回调输入timeout2以及微工作Promise外面的setTimeout,输入timeout resolved。(这里要阐明的是 微工作nextTick优先级要比Promise要高)

5. 最初案例

代码片段1:

setImmediate(function(){
  console.log("setImmediate");
  setImmediate(function(){
    console.log("嵌套setImmediate");
  });
  process.nextTick(function(){
    console.log("nextTick");
  })
});

/*     C:\Users\92809\Desktop\node_test>node test.js    setImmediate    nextTick    嵌套setImmediate*/

解析:

事件循环check阶段执行回调函数输入setImmediate,之后输入nextTick。嵌套的setImmediate在下一个事件循环的check阶段执行回调输入嵌套的setImmediate

代码片段2:

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout0') 
},0)  
setTimeout(function(){
    console.log('setTimeout3') 
},3)  
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function(){
    console.log('promise3')
})
console.log('script end')

打印后果为:

C:\Users\92809\Desktop\node_test>node test.js
script start
async1 start
async2
promise1
promise2
script end
nextTick
promise3
async1 end
setTimeout0
setTimeout3
setImmediate

大家呢,能够先看着代码,默默地在心底走一变代码,而后比照输入的后果,当然最初三位,我集体认为是有点问题的,毕竟在主模块运行,大家的答案,最初三位可能会有偏差;

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据