node事件循环和浏览器事件循环完全不一样
Node事件循环阶段
- timers(定时器):此阶段执行
setTimeout
和setInterval
调度的回调函数 - I/O callback(I/O回调):此阶段执行几乎所有的回调函数,除了
close callbacks(关闭回调)
和 由timers、setImmediate
调度的回调 - idle(空转):此阶段只在node内部使用
- poll(轮询):检索新I/O事件,在恰当的时候Node会阻塞在这个阶段
- check(检查):
setImmediate
的回调会在此阶段被调用 - close callbacks(关闭事件的回调):例如
socket.on('close', cb)
的回调会在此阶段被调用
在这些阶段中,比较重要且复杂的是poll阶段
- 如果poll队列不为空,将会同步的执行队列中的回调,直到队列为空或回调到达系统上限
- 如果poll队列为空
- 并且代码中有
setImmediate
的回调,那么将结束poll阶段,执行check阶段的队列(就是setImmediate
的回调) - 并且代码中没有
setImmediate
的回调,事件循环会阻塞 在该阶段,等待回调加入到队列中,一旦有回调加入就执行
- 并且代码中有
- 如果进入poll阶段,且代码中有timers
- 如果poll处于空闲,事件循环将会检查timers,如果有timers已经到了执行的时间,事件循环就会按顺序进入timers阶段,并执行timers队列(也就是进入下一阶段的事件循环)
代码一
const fs = require('fs')
const path = require('path')
function asyncOperation(cb) { //模拟读取文件操作
//假设读取文件花费2ms
fs.readFile(path.resolve(__dirname, 'xx.txt'), cb)
}
setTimeout(() => {
console.log('setTimeout');
}, 10)
asyncOperation(function() {
let fileReadTime = Date.now()
while(Date.now() - fileReadTime < 20) { //此处会卡住20ms
}
})
分析:
- 首先执行setTimeout,将回调放入I/O队列,他要10ms之后才执行,然后执行到asyncOperation函数,进行文件读取操作,需要2ms读取完成后执行回调,这个回调也放入到I/O中,所以此时timers阶段没有需要执行的回调。
- 来到I/O callback(I/O回调),没有需要执行的回调
- 来到poll阶段,现在的时间是0ms。此时poll队列为空,事件循环阻塞在poll阶段
- 时间为2ms时,读取文件操作完毕,读取文件的回调进入poll队列被立即执行。而这个回调因为内部的while循环需要执行20ms
- 到10ms时,setTimeout已经到了执行的时间,但是由于js为单线程且读取文件的回调占用了这个线程,所以setTimeout的回调无法执行
- 当22ms时,读取文件的回调执行完毕,此时poll队列为空且有timers。所以setTimeout回调会被加入到第二轮事件循环的timers阶段中,然后执行这个回调
代码二
setTimeout(function timeout() {
console.log('timeout');
}, 1)
setImmediate(function immediate() {
console.log('immdeiate');
})
结果:上述代码的执行顺序不确定
不确定的原因:事件循环的启动也需要事件,可能执行到poll阶段已经超过1ms,此时setImmediate在check阶段被执行,先于setTimeout。反之setTimeout先执行
nextTick
process.nextTick()
不在事件循环的任何阶段执行,而是在各个阶段切换的中间执行
设计原因:允许开发之通过调用process.nextTick()
来阻塞I/O
示例代码:来看看下面代码的打印结果
const fs = require('fs')
const path = require('path')
fs.readFile(path.resolve(__dirname, 'read.txt'), () => { //假设读取文件耗时2ms
setTimeout(() => {
console.log('setTimeout');
}, 0)
setImmediate(() => {
console.log('setImmediate');
process.nextTick(() => {
console.log('nextTick3');
})
})
process.nextTick(() => {
console.log('nextTick1');
})
process.nextTick(() => {
console.log('nextTick2');
})
})
//nextTick1
//nextTick2
//setImmediate
//nextTick3
//setTimeout
分析:
- 由于读取文件需要耗时,所以在timers和I/O回调阶段没有要执行的回调,事件循环被阻塞在poll阶段
- 当文件读取完成,将setTimeout的回调加入I/O队列。此时poll还是为空,且有setImmediate回调,所以从poll阶段要走到check阶段,在这期间执行两个nextTick,打印出
nextTick1
和nextTick2
- 来到check阶段,执行setImmediate回调,打印
setImmediate
- 从check阶段进入下一轮事件循环的timers阶段之间执行nextTick,打印
nextTick3
- 来到第二轮事件循环的timers,在I/O队列的setTimeout回调加入到timers中并执行,打印
setTimeout
最后
我的博客网站上有我更多的学习成果
本文含有隐藏内容,请 开通VIP 后查看