Node事件循环

440 阅读3分钟

node

JavaScript是一门单线程语言,主线程中的代码必须依次执行,后面的代码必须等待前面的代码执行完毕,这样容易造成阻塞。而回调函数的使用使得部分代码可以让出主线程,node事件循环就是解决了这样的一个问题。

  • node事件循环机制是基于libuv实现的。
  • libuv是一个高性能、事件驱动、非阻塞型I/O类库,并且提供了跨平台。
  • node事件循环也分为宏任务和微任务。
  • 宏任务有:setTimeout、setInterval、setImmediate、I/O操作等。
  • 微任务有:Promise、process.nextTick()(优先级更高)

node事件循环的流程图:

1.png

  • timers:本阶段执行已被setTimeout()和setInterval()调度的回调函数,即由这两个函数启动的回调函数。
  • pending callbacks:本阶段执行某些系统操作的回调函数。
  • idle,prepare:仅系统内部使用,只需知道即可。
  • poll:检索新的I/O事件,执行于I/O相关的回调( 几乎所有情况下),其余情况 node 将在适当的时候在此阻塞。
  • check:setImmediate()回调函数在这里执行,setTimediate()并不是立马执行而是当事件poll中没有新的事件处理就会执行该部分。
  • 执行一些关闭的回调函数,如:socket.on()...
const fs = require('fs')
//默认为1ms
setTimeout(()=>{
    console.log('1');
},0)
​
setImmediate(()=>{
    console.log('setImmediate');
})
//readFile先执行,但是执行之间大于1ms,因此后输出
fs.readFile('./data.md',(err,data)=>{
    if(err)    throw err;
    console.log('readFile success');
})
​
Promise.resolve.then(()=>{
    console.log('callback');
})
console.log('2');
​
//2 callback 1 setImmediate readFile success

运行起点

  • Node.js启动时,也就是node启动是就会发起一个新的事件循环。
  • setTimeout回调函数。
  • setInterval回调函数。
  • 也可能时一次I/O后的回调函数。

执行过程

2.png

在同一个事件循环中,先执行完微任务队列再执行去执行宏任务队列。

版本不同

timers 阶段的执行时机变化

setImmediate(() => {
    console.log('timeout1')
    Promise.resolve().then(() => console.log('promise resolve'))
    process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
  • 在 node11 之前,因为每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
  • 在 node11 之后,process.nextTick 是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,再执行下一个宏任务及其微任务,因此输出为timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

check 阶段的执行时机变化

setImmediate(() => console.log('immediate1'));
setImmediate(() => {
    console.log('immediate2')
    Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
  • node11: immediate1=>immediate2=>promise resolve=>immediate3=>immediate4

  • node10: immediate1=>immediate2=>immediate3=>immediate4=>promise resolve

总结就是: 如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列。

node 和 浏览器 eventLoop的主要区别

可以看出最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。

单线程/多线程

主线程是单线程的,但Node.js存在多线程执行,多线程包括Settimeout、异步I/O事件,其实还有其它的线程,包括垃圾回收,内存优化等。