下面的分析都是基于 Node.js 10+ 版本的
1、Node.js循环原理图
- timers 阶段:这个阶段执行 setTimeout 和 setInterval 的回调函数。
- pending callbacks 阶段:不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了所有回调函数。
- idle, prepare 阶段:系统内部使用
- poll 阶段:获取新的 I/O 事件。在某些场景下 Node.js 会阻塞在这个阶段。
- check阶段:执行 setImmediate() 的回调函数。
- close callbacks 阶段:执行关闭事件的回调函数,如 socket.on(‘close’, fn) 里的 fn。
执行代码过程中,大部分时间会停留在poll阶段,不断的对操作系统进行轮询。
当node程序结束时,Node.js会检查EventLoop是否在等待异步I/O结束,是否在等待计时器触发,如果没有,就会关掉 event loop。
2、运行起点
setTimeout(() => {
console.log('1')
}, 0)
console.log('2')
// 2
// 1
看事件循环的起点是timer阶段,但是我们这里先输入的是2?
原因是Node.js启动后,会初始化事件循环,处理已提供的脚本,它可能会先调用一些异步的 API、调度定时器,或者 process.nextTick(),然后再开始处理事件循环。那么运行的起点:Node.js进程启动后,就发起一个新的事件循环
3、Node.js 事件循环(poll阶段)
下面这个过程就是poll阶段了,主要处理异步I/O的回调函数,异步I/O在Node中一般分为网络I/O和文件I/O
微任务:在 Node.js 中微任务包含 2 种——process.nextTick 和 Promise。微任务在事件循环中优先级是最高的,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise 也存在优先级,process.nextTick 高于 Promise
宏任务:在 Node.js 中宏任务包含 4 种——setTimeout、setInterval、setImmediate 和 I/O。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列
4、setImmediate vs setTimeout
两者区别在于回调时机不一致,setImmediate() 的作用是在当前 poll 阶段结束后调用一个函数,setTimeout() 的作用是在一段时间后调用一个函数。
如果 setTimeout 和 setImmediate 都是在主模块(main module)中被调用的,那么回调的执行顺序取决于当前进程的性能,两个回调的执行顺序我们无法判断
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
如果是刚进入程序,那么按照上面的流程图,此时限制性timeout,后执行immediate;若执行这段代码的时候event loop处于poll状态(大部分时间如此),那么就限制性immediate,后执行timeout
我们将上面的代码放到IO操作中进行回调,setImmediate的回调总是犹豫setTimeout的回调
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate');
})
})
5、单线程/多线程
主线程是单线程执行的,但是 Node.js 存在多线程执行,多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化等。
主要是主线程来循环遍历当前的事件,因此我们常说Node.js 是以事件驱动,来处理I/O密集型的高并发的语言