示例
从实践出发,先看一个例子
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
看一下在node环境下的输出结果
timer1
timer2
promise1
promise2
先简单分析一下执行过程:
首先进入 timers 阶段,执行timer1的回调函数,打印timer1
,并将promise1.then 回调放入 microtask 队列,同样的步骤执行timer2,打印timer2
;至此,timer阶段执行结束,event loop 进入下一个阶段之前,microtask 队列的所有任务依次打印promise1
、promise2
。
Node.js的事件处理
Node.js采用V8作为js的解析引擎,而I/O处理使用了自己设计的libuv。什么是libuv,可以查看这篇文章(libuv介绍) 根据Node.js官方介绍,每次循环都包含6个阶段,如图所示:
- timers 阶段: 执行
setTimeout
和setInterval
中到期的callback
- I/O callbacks 阶段: 上一轮循环中的少数的
callback
会放在这一阶段执行 - idle, prepare 阶段: 仅在node内部使用
- poll 阶段: 最重要的阶段,获取新的I/O事件,适当条件下node会阻塞在这个阶段
- check 阶段: 执行
setImmediate()
的回调(setImmediate
将事件插入到事件队列的尾部,主线程和事件队列的函数执行完之后立即执行setImmediate
) - close callbacks 阶段: 执行
socket
和close
的回调函数,比如socket.on('close', callback)
的 callback 会在这个阶段执行
每一个阶段都有一个装有 callback 的fifo queue(队列)
,当 event loop 运行到一个指定阶段时,Node将执行该阶段的队列,当队列 callback 执行完或执行 callbacks 数量超过该阶段上限时,event loop 会转入下一个阶段。
重点看timers
、poll
、check
3个阶段,具体细节如下:
timer
timer是事件循环的第一个阶段,Node会去检查setTimeout
和setInterval
中有无过期的callback
,如果有则把它的回调压入timer的任务队列中等待执行。
poll
在nide.js里,任何异步方法(除timer
、close
、setImmediate
外)完成时,都会将其 callback 加到 poll queue 里,并立即执行。
poll阶段主要有2个功能:
- 处理 poll 队列的事件
- 当到达 timers 指定的时间时,执行 timers 的 callback
event loop进入poll阶段会出现两种情况:
1.timer queue 为空
- 如果 poll queue 不为空,event loop 将同步执行队列中的 callback,直到队列为空或者执行的 callback 到达系统上限
- 如果 poll queue 为空,又分为两种情况:
- 如果有预设的
setImmediate()
, event loop 将结束 poll 阶段进入 check 阶段,并执行 check 阶段的任务队列 - 如果没有预设的
setImmediate()
, event loop 将阻塞在该阶段等待 callbacks 加入 poll queue
- 如果有预设的
2.timer queue 不为空
- 如果 poll queue 进入空状态,event loop 将检查 timers ,如果有 timers 时间已到达,event loop 将按循环进入 timers 阶段,并执行 timer queue
check
此阶段允许在 poll 阶段完成后立即执行回调。如果 poll 阶段空闲并且队列中有setImmediate()
,event loop 会继续执行到 check 阶段而不是继续等待。
setImmediate() 和 setTimeout() 的区别
setImmediate()
用于在当前poll
阶段完成后check
阶段执行脚本setTimeout()
在经过以毫秒(ms)为单位的最小阙值后,在timers
阶段执行
举个例子:
const fs = require('fs')
fs.readFile('a.txt', () => {
console.log('readFile')
setTimeout(() => {
cosnole.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
}, 0)
})
执行结果
readFile
immediate
timeout
immediate
在timeout
之前打印,主要原因是在I/O
阶段读取文件后,event loop 会先进入poll
阶段,发现有setImmediate()
需要执行,会立即进入check
阶段执行setImmediate()
的回调,然后再进入timers
阶段,执行setTimeout()
的回调。
process.nextTick()
process.nextTick()
会在各个事件阶段之前执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段。所以如果递归调用process.nextTick()
会导致 I/O starving 的问题。
例子:
setTimeout(() => {
console.log('timeout')
})
setImmediate(() => {
console.log('immediate')
})
function apiCall(cb) {
process.nextTick(cb)
}
apiCall(() => {
console.log('process.nextTick')
})
打印结果为
process.nextTick
immediate
timeout
// 或者
// process.nextTick
// timeout
// immediate
参考文章:
1.event-loop-timers-and-nexttick
2.深入理解js事件循环机制(Node.js篇)
3.一次弄懂Event Loop
4.Node.js Event Loop 的理解 Timers,process.nextTick()