Node.js的事件循环机制(Event Loop)

在浏览器存在着事件循环,Node.js也同样存在事件循环,那么两个事件循环有什么区别呢?我们先看一下下面的代码输出了些什么吧。

setTimeout(()=>{
    console.log(1)
    Promise.resolve().then(function() {
        console.log(2)
    })
}, 0)

setTimeout(()=>{
    console.log(3)
    Promise.resolve().then(function() {
        console.log(4)
    })
}, 0)

在浏览器上,我们输出的结果为1 => 2 => 3 => 4

而在Node 12版本(包括)之后,我们输出的结果为1 => 2 => 3 => 4,和浏览器基本一致

Node.js的事件循环机制的六个阶段

与浏览器不同的是,浏览器的Event Loop是HTML5中定义的规范,而Node.js采用V8引擎,其中的Event Loop是依赖libuv库实现的,libuv是一个基于事件驱动的跨平台抽象层,封装了许多实用的API,事件循环的六个阶段就是在这里面编写的。

timers 阶段:这个阶段检查是否有到期的timer(setTimeout、setInterval),并执行相应的回调函数。
pending callbacks 阶段:该阶段执行某些系统操作的回调,比如TCP套接字在连接时收到ECONNREFUSED。
idle, prepare 阶段:仅系统内部使用。
poll 阶段:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
check 阶段:执行setImmediate的回调函数。
close callbacks 阶段:一些准备关闭的回调函数,如:socket.on(‘close’, …)

Node.js 中,先执行全局Script代码,执行完同步代码调用栈清空后,先从微任务队列Next Tick Queue中依次取出所有的任务放入调用栈中执行,再从微任务队列Other Microtask Queue中依次取出所有的任务放入调用栈中执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行(注意,这里和浏览器不一样,浏览器只取一个,所以从任务调度的角度上看,nodejs会省时一点),每个宏任务阶段执行完毕后,开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环。

const fs = require('fs');
console.log('1.同步代码!'); // 同步代码一定比异步代码先执行
process.nextTick(() => { // 整个函数我们理解为一个宏任务,当整个代码执行完,
// 会执行此宏任务产生的微任务,process.nextTick 会立即执行  console.log('3.process.nextTick!');
});
setImmediate(() => { // check阶段执行
  console.log('5.setImmediate!');
});
Promise.resolve().then(() => { // 微任务,等待宏任务执行完毕后执行,执行顺序在process.nextTick 之后
  console.log('4.Promise.resolve then!');
});
setTimeout(() => { // 1s后会将此会将此setTimeout的回调推送到 timer阶段对应的task queue中
  console.log('7.setTimeout');
  process.nextTick(() => {
    console.log('8.setTimeout process.nextTick!');
  });
  Promise.resolve().then(() => {
    console.log('9.setTimeout Promise.resolve then');
  });
}, 1000);
fs.readFile('./event.js', () => { // I/O 回调在poll阶段执行,具体何时执行以来fs.readFile异步读取文件的所需时间
  console.log('6.fs.readFile');
});
console.log('2.同步代码!');


// 以下是输出:// 1.同步代码!
// 2.同步代码!
// 3.process.nextTick!
// 4.Promise.resolve then!
// 5.setImmediate!
// 6.fs.readFile
// 7.setTimeout
// 8.setTimeout process.nextTick!
// 9.setTimeout Promise.resolve then

几个问题

1.setTimeout && setImmediate执行顺序

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

分两种情况

1.当 setTimeout() 和 setImmediate() 都写在主线程中不一定谁先执行谁后执行;这个时候受线程性能的影响。setTimeout 0 在node中会被设置为1ms后执行。事件循环的准备需要时间,如果事件循环的准备时间大于1ms那 setTimeout 0先执行。反之 setImmediate 先执行。

setTimeout(function () {
  process.nextTick(function() {
    console.log('next tick');
  });
  
  setTimeout(function() {
    console.log('settimeout');
  });
  
  (async function() {
    console.log('async promise');
  })();
  
  setImmediate(function() {
    console.log('setimmediate');
  });
})
async promise
next tick
setimmediate
settimeout

2.当 setTimeout() 和 setImmediate() 都写在一个 I/O 回调 或者说一个 poll 类型宏任务的回调里面的时候 一定是先执行 setImmediate() 后执行 setTimeout(),因为poll后面是check

2.Poll 阶段的两个主要功能

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

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

处理该阶段队列中的事件

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

你可能感兴趣的:(node)