事件循环是node处理非阻塞I/O操作的机制。
当node.js
启动后,会初始化事件轮询;执行脚本,然后启动事件循环。
概览
下图是事件循环处理顺序的概览:
每个阶段都有一个FIFO队列来执行回调。当事件循环进入一个阶段,会把该阶段的回调执行完或者达到最大回调数,才会移动到下一个阶段。阶段概述
- timers: 执行
setTimeout()
和setInterval()
的回调函数。 - pending callbacks: 执行延迟到下一次循环的I/O回调。
- idle, prepare: 仅供内部使用
- poll: 检索新的I/O事件;执行I/O相关回调。node将会在此处阻塞。
- check:
setImmediate()
回调 - close callbacks: 一些关闭的回调函数,如:
socket.on('close', ...)
。
阶段详解
- timers:给出可以执行回调的最少时间阈值。
- poll: 有两个任务: 1)计算应该阻塞和轮询I/O 2)执行轮poll列里面的事件
当事件循环进入poll阶段,有两种可能: 1)poll队列不是空的,那么事件循环会将队列里面的回调一个一个执行,直到队列用尽,或者达到最高回调数。 2)poll是空的!,则会去检查有无check阶段回调等待,有的话,移动到check阶段执行。有无到达时间的timers回调,有的话,进入timer阶段执行。如果都没有,则会停留在这个poll阶段,等待回调,并且在有回调进入的时候,立即执行。
setImmediate()
对比setTimeout()
处在事件循环不同阶段,执行时机不同。
使用 setImmediate()
相对于setTimeout()
的主要优势是,如果setImmediate()
是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关,因为poll阶段之后,直接就是check阶段了。
process.nextTick()
process.nextTick()
从技术上讲不是事件循环的一部分,因此并未出现在上图事件循环中。无论在哪个阶段,process.nextTick()
回调都会在当前回调完成后处理。
这样机制下,如你过递归调用process.nextTick()
,就会'饿死'正常I/O轮询里的回调。
代码示例
const fs = require('fs')
const path = require('path')
console.log('1')
let promise = new Promise(function(resolve){
console.log('new promise')
resolve(1)
})
fs.readFile(path.join(__dirname, './ling.js'), () => {
console.log('readFile')
process.nextTick(function(){
console.log('tick insert')
})
promise.then(function(){
console.log('promise insert')
})
setTimeout(() => {
console.log('timeout')
process.nextTick(function(){
console.log('tick insert in timer')
})
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
fs.readFile(path.join(__dirname, './ling.js'), () => {
console.log('readFile2')
setTimeout(() => {
console.log('timeout2')
}, 0)
setImmediate(() => {
console.log('immediate2')
})
})
promise.then(function(){
console.log('promise then1')
promise.then(function(){
console.log('promise then2')
promise.then(function(){
console.log('promise then3')
})
})
})
process.nextTick(function(){
console.log('tick 1')
process.nextTick(function(){
console.log('tick 3')
})
})
process.nextTick(function(){
console.log('tick 2')
})
console.log('end')
执行结果
1
new promise // 同步执行,只有.then是异步
end // 第一次同步执行完毕
tick 1 // nextTick队列优先级最高,先一次性清空
tick 2
tick 3
promise then1 // 和nextTick类似,但是优先级没nextTick高
promise then2 // 也是先一次性清空
promise then3
readFile // 进入事件循环poll阶段
tick insert // 执行完poll阶段的一个回调之后,就会去检查nextTick
promise insert // 和 promise的队列有没有回调,有的话执行
readFile2 // 执行完上面的再继续poll阶段下一个回调
immediate // 进入check阶段
immediate2 // check阶段全部执行完
timeout // 进入timers阶段
tick insert in timer // tiemrs执行完一个回调,就会去检查nextTick,和promise
timeout2 // 继续执行timers阶段
和浏览器事件循环对比
差异: node的事件循环和浏览器的还是有差异的,特别是node事件循环存在几个阶段。
一致: 在处理promise.then 和process.nextTick 这些微任务,规则是一样的:就是在任何时候,只要处理完一个回调,就会去检查,有没有这些微任务,有的话,就一次性执行完毕。再回去继续原来。