Node的事件循环机制
浏览器的 Event-Loop 由各个浏览器自己实现;而 Node 的 Event-Loop 由 libuv 来实现。
libuv 主导循环机制共有六个循环阶段,重点是以下三个阶段:
- timers(计时器阶段): 初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务。timers 阶段的setTimeout、setInterval等函数派发的任务、包括 setImmediate 派发的任务,在Node11版本之后都被修改为:一旦执行完当前阶段的一个任务,就立刻执行微任务队列。
- poll (轮询阶段): 重点阶段,这个阶段会执行I/O回调,同时还可能回到Timer阶段执行回调。进入poll阶段时,有两种场景:
(1)poll 队列不为空。则直接逐个执行队列内的回调并出队、直到队列被清空
(2)poll 队列本来就是空的。则它首先会检查有没有待执行的 setImmediate 任务,如果有,则往下走、进入到 check 阶段开始处理 setImmediate;如果没有 setImmediate 任务,那么再去检查一下有没有到期的 setTimeout 任务需要处理,若有,则跳转到 timers 阶段。 - check(检查阶段): 处理 setImmediate 中定义的回调。
Node中的宏任务和微任务
常见的 macro-task 比如: setTimeout、setInterval、 setImmediate、 script(整体代码)、I/O 操作、UI渲染等。
常见的 micro-task 比如: process.nextTick、Promise、MutationObserver 等。其中,Node 在执行微任务时, 优先执行 next-tick 队列中的任务,随后才会清空其它微任务。
例题1
Promise.resolve().then(function() {
console.log("promise1")
}).then(function() {
console.log("promise2")
});
process.nextTick(() => {
console.log('nextTick1')
process.nextTick(() => {
console.log('nextTick2')
process.nextTick(() => {
console.log('nextTick3')
process.nextTick(() => {
console.log('nextTick4')
})
})
})
})
// 首先进入主线程,派发了promise和process.nextTick两种微任务,process.nextTick优先执行。
// nextTick1 nextTick2 nextTick3 nextTick4 promise1 promise2
例题2
浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。(但是node11版本之后和浏览器已经一样了,timers 阶段的setTimeout、setInterval等函数派发的任务、包括 setImmediate 派发的任务,都被修改为:一旦执行完当前阶段的一个任务,就立刻执行微任务队列。)
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
// start
// end
// promise3
// timer1
// promise1
// timer2
// promise2
// 首先主线程阶段,执行同步任务(属于宏任务),打印start,end,并派发了一个微任务,执行其回调函数
// 接着timer阶段,打印timer,并派发一个微任务执行,打印promise1,打印timer2,并派发微任务执行,打印promise2.(这里node11版本之后和浏览器已经一样了,timers 阶段的setTimeout、setInterval等函数派发的任务、包括 setImmediate 派发的任务,都被修改为:一旦执行完当前阶段的一个任务,就立刻执行微任务队列。)
例题3
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
// 主线程之后直接进入了poll阶段,执行io操作,派发了一个setTimeout和setImmediate任务,由于check阶段比timer阶段进,所以先进入check阶段执行setImmediate的回调函数。
注意:
setImmediate()方法用于中断长时间运行的操作,并在完成其他操作(如事件和显示更新)后立即运行回调函数。
setTimeout(function() {
console.log('老铁,我是被 setTimeout 派发的')
}, 0)
setImmediate(function() {
console.log('老铁,我是被 setImmediate 派发的')
})
// setImmediate 和 setTimeout 谁先执行是个谜,它是随机的!
首先,setTimeout 这个函数的第二个入参,它的取值范围是 [1, 2^31-1]。也就是说,它是不认识 0 这个入参的,会强行为1,也就是说这个回调被延迟了1ms。
然后,事件循环的初始化,是需要时间的,这个时间可能大于1ms,也可能小于1ms。
因此:
- 当初始化时间小于 1ms 时:进入了 timers 阶段,却发现 setTimeout 定时器还没到时间,于是往下走。走到 check 阶段,执行了 setImmediate 回调;在后面的循环周期里,才会执行 setTimeout 回调;
- 当初始化时间大于 1ms 时:进入了 timers 阶段,发现 setTimeout 定时器已经到时间了,直接执行 setTimeout 回调;结束 timers 阶段后,走啊走,走到了 check 阶段,顺理成章地又执行了 setImmediate 回调。