NodeJS事件循环
什么是事件循环
事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。浏览器事件循环与Nodejs事件循环的区别
Nodejs事件循环的简化流程
图片来自Nodejs官网

阶段概述
阶段代表的含义
需要注意的点
什么时候会停留?当timers阶段没有注册的回调函数,以及check阶段没有注册的回调函数的时候,事件循环会在poll阶段进行短暂的停留,等待新的I/O操作。
什么时候不停留?当timers阶段和check阶段的回调队列不为空的时候就会不做暂停,事件循环会从poll轮询阶段跳转到check阶段
代码示例
Nodejs如何进行事件循环的
示例1
console.log('start')
setTimeout(() => {
console.log('1')
})
setTimeout(() => {
console.log('2')
})
console.log('end')
// 输出 start end 1 2
// 解释:当代码输入完毕开始运行,console.log('start')属于同步任务,会直接执行。当遇到第一个setTimeout函数会将回调函数注册到 timers 阶段的回调队列中,第二个setTimeout同理,然后再遇到第二个console函数,同步执行。 同步执行完毕,进入事件循环,根据上面概述,事件循环始于timers阶段,且这两个setTimeout都没有设置ms参数,所以会一次根据先进先出的规则输出 1和2
// setTimeout第二个参数 不写或者传人0都会被node强制改为4ms
示例2
setTimeout(() => {
console.log(1)
}, 0)
setImmediate(() => {
console.log(2)
})
function sleep(duration) {
let start = new Date().getTime()
while(new Date().getTime() - start < duration) {
continue
}
}
sleep(1000)
// 1, 2
// 2, 1
// 以上两种情况都有可能。
// 解释:代码输入后,遇到第一个setTimeout函数,会将其回调注册到timers阶段,之后遇到setImmediate函数,会将其回调注册到check阶段,之后执行同步sleep方法,所以上面的代码会在1000ms后才会有输出
// setTimeout 函数的 回调时间设置为0,这个上面讲过,会被node强制修改掉,所以在当前event loop中如果还没到回调时间,那么就会跳过 timers阶段,然后走到checke阶段,这种情况下就会先输出2, 再输出1, 如果当前event loop过程中,setTimeout的回调函数到了,那么就会先输出1, 再输出2。
// 为什么会出现这种情况,完全取决于你当前机器的状态。
示例3
const fs = require('fs')
fs.readFile('./index.html', () => {
setTimeout(() => {
console.log(1)
}, 0)
setImmediate(() => {
console.log(2)
})
})
// 输出 2, 1
// 疑问: 这段代码不管怎么样都会输出2, 1。同样都是setTimeout 和 setImmediate, 看起来与示例2的代码没有太大的区别, 为什么会一定输出2, 1呢?
// 解释: 由于这段代码 只有一个同步读取文件的函数方法,所以这段代码输入后,时间循环会直接跳入到poll轮训阶段,poll阶段主要出里I/O操作,所以这段代码在读取完文件后,会执行后续的回调函数,会将console.log(1)注册到timers阶段,console.log(2)注册到checke阶段。然后当前poll阶段的回调执行完毕,那么时间循环会跳转到check阶段(看流程图),跳转到check阶段后,发现check阶段已经有注册过的console.log(2)函数,那么会先进先出的执行回调函数。check阶段的回调执行完后,当前event loop执行完毕。然后从timers开起新的event loop, 发现timers阶段已经有回调,那么就执行回调,所以这段代码的输出一定会是2, 1这样子。
示例4
setTimeout(() => {
console.log(1)
}, 60)
process.nextTick(() => {console.log(2)})
setImmediate(() => {console.log(3)})
process.nextTick(() => {console.log(4)})
// 输出: 2 4 3 1
// 解释: 都是异步API
// 1. 将console.log(1) 注册到timers阶段
// 2. 将console.log(2) 微任务会被注册到微任务的事件队列中
// 3. 将console.log(3) 注册到check阶段
// 4. 将console.log(4) 同样微任务会被注册到微任务的事件队列中
// 由于微任务优先级高于宏任务,当前微任务队列里有两个注册的回调函数,根据FIFO规则,所以先输出2, 再输出4。微任务队列清空,开始执行宏任务,事件循环从timers阶段开始,但是由于 设置了60ms的回调时间,所以时间没到,那么时间循环就会掉过当前timers阶段,跳入到check阶段,当前阶段有回调函数,接着就输出3。当回调的60ms还没到的时候,node会不停的进行事件循环的扫描,一轮又一轮,当60ms到了,那么就会执行timers阶段的回调函数,此时会输出1。
// 至此输出 2 4 3 1
示例5
setTimeout(() => {
console.log(1)
}, 60)
setImmediate(() => {console.log(2)})
process.nextTick(() => {console.log(3)})
Promise.resolve().then(() => console.log(4))
;(() => console.log(5))()
// 输出 5 3 4 2 1
// 解释: 先输出5就不用解释了,nextTick优先级高于Promise,所以输出3, 4也没有问题,剩下的输出2,1。具体可以参考示例4
示例6
process.nextTick(() => {console.log(1)})
Promise.resolve().then(() => console.log(2))
process.nextTick(() => {console.log(3)})
Promise.resolve().then(() => console.log(4))
// 输出: 1 3 2 4
// 解释: 不解释
示例7
setTimeout(() => {
console.log(1)
}, 50)
process.nextTick(() => {console.log(2)})
setImmediate(() => {console.log(3)})
process.nextTick(() => {
setTimeout(() => {
console.log(4)
}, 1000)
})
// 输出:2 3 1 4
// 解释:根据之前示例的讲解,先执行微任务,输出2, 然后在第二个微任务回到中执行了setTimeout函数,此时timers的队列中存在了2个回调函数。微任务执行完毕接下来执行宏任务,由于timers两个阶段的函数都设置了回调时间,所以优先会执行check阶段队列中的回调,所以输出3,接下来根据回调时间的快慢执行,先输出1,再输出4。